From 4aa62722c6673f9c3b28a2bd979c9818af938e5f Mon Sep 17 00:00:00 2001 From: "Alexander R." <118708187+Dewm-Bot@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:51:54 +0000 Subject: [PATCH] Folder support, Custom theme support, Boiler Plate Code support --- config.json | 1 + config.json.local | 1 + frontend/src/api/client.ts | 35 +- frontend/src/pages/Workspace.tsx | 568 +++++++++++- scratch/convert_themes.py | 61 ++ scratch/test_command_palette.py | 27 + scratch/test_multifile.py | 45 + scratch/test_textual_theme.py | 17 + scratch/test_tui_theme.py | 20 + src/api/main.py | 38 +- src/core/config.py | 12 +- src/core/executor.py | 85 +- src/core/linter.py | 40 +- src/core/mentor.py | 30 +- src/core/prompts.py | 82 +- src/tui/app.py | 837 ++++++++++++++++-- src/tui/theme_manager.py | 154 ++++ src/tui/themes/adw.json | 12 + src/tui/themes/atuel.json | 12 + src/tui/themes/ayu-blue.json | 12 + src/tui/themes/ayu-green.json | 12 + src/tui/themes/ayu-red.json | 12 + src/tui/themes/breeze.json | 12 + src/tui/themes/catppuccin-frappe-blue.json | 12 + .../themes/catppuccin-frappe-lavender.json | 12 + src/tui/themes/catppuccin-frappe-mauve.json | 12 + src/tui/themes/catppuccin-frappe-pink.json | 12 + .../themes/catppuccin-frappe-rosewater.json | 12 + .../catppuccin-frappe-sapphirejson.json | 12 + src/tui/themes/catppuccin-lavender.json | 12 + .../themes/catppuccin-macchiato-lavender.json | 12 + .../themes/catppuccin-macchiato-mauve.json | 12 + src/tui/themes/catppuccin-macchiato-pink.json | 12 + .../catppuccin-macchiato-rosewater.json | 12 + .../themes/catppuccin-macchiato-sapphire.json | 12 + src/tui/themes/catppuccin-mocha-pink.json | 12 + .../themes/catppuccin-mocha-rosewater.json | 12 + src/tui/themes/catppuccin-mocha-sapphire.json | 12 + src/tui/themes/catppuccin.json | 12 + src/tui/themes/cherry-blossom.json | 12 + src/tui/themes/cream-autumn.json | 12 + src/tui/themes/cream.json | 12 + src/tui/themes/creamy-forest.json | 12 + src/tui/themes/cyberpunk.json | 12 + src/tui/themes/default.json | 12 + src/tui/themes/doomed.json | 12 + src/tui/themes/dracula.json | 12 + src/tui/themes/espresso-cream.json | 12 + src/tui/themes/everdeer.json | 12 + src/tui/themes/everforest-alt.json | 12 + src/tui/themes/everforest-material.json | 12 + src/tui/themes/everforest.json | 12 + src/tui/themes/flexoki.json | 12 + src/tui/themes/garnet.json | 12 + src/tui/themes/github-dark.json | 12 + src/tui/themes/gruber-darker.json | 12 + src/tui/themes/gruvbox-material.json | 12 + src/tui/themes/gruvbox.json | 12 + src/tui/themes/gruvboxalt.json | 12 + src/tui/themes/hexa34c.json | 12 + src/tui/themes/horizon.json | 12 + src/tui/themes/jiva-lotus.json | 12 + src/tui/themes/jiva-paper.json | 12 + src/tui/themes/jiva.json | 12 + src/tui/themes/kanagawa-dragon.json | 12 + src/tui/themes/kanagawa-kasumi.json | 12 + src/tui/themes/kanagawa-paper.json | 12 + src/tui/themes/kemuri-koke.json | 12 + src/tui/themes/kemuri-susu.json | 12 + src/tui/themes/kemuri.json | 12 + src/tui/themes/lilac-amoled.json | 12 + src/tui/themes/macaron.json | 12 + src/tui/themes/matecito.json | 12 + src/tui/themes/miasma.json | 12 + src/tui/themes/mine.json | 12 + src/tui/themes/mizuki-akiyama.json | 12 + src/tui/themes/monochrome.json | 12 + src/tui/themes/murasaki.json | 12 + src/tui/themes/murata.json | 12 + src/tui/themes/naysayer.json | 12 + src/tui/themes/neon-surf.json | 12 + src/tui/themes/noctalia-legacy.json | 12 + src/tui/themes/nord-aurora.json | 12 + src/tui/themes/nord.json | 12 + src/tui/themes/oasis-abyss.json | 12 + src/tui/themes/occult-umbral.json | 12 + src/tui/themes/one-dark-two.json | 12 + src/tui/themes/one.json | 12 + src/tui/themes/osaka-jade.json | 12 + src/tui/themes/oxide.json | 12 + src/tui/themes/oxocarbon.json | 12 + src/tui/themes/paradise.json | 12 + src/tui/themes/peche.json | 12 + src/tui/themes/rose-pine-alt.json | 12 + src/tui/themes/rose-pine-moon-alt.json | 12 + src/tui/themes/rose-pine-moon.json | 12 + src/tui/themes/rosey-amoled.json | 12 + src/tui/themes/shien.json | 12 + src/tui/themes/shinonome.json | 12 + src/tui/themes/solarized-osaka.json | 12 + src/tui/themes/solarized.json | 12 + src/tui/themes/sway-classic.json | 12 + src/tui/themes/tokyo-night-moon.json | 12 + src/tui/themes/tokyo-night-storm.json | 12 + src/tui/themes/tokyo-night.json | 12 + src/tui/themes/vesper.json | 12 + src/tui/themes/zenbones.json | 12 + src/tui/widgets.py | 39 + 108 files changed, 3020 insertions(+), 152 deletions(-) create mode 100644 scratch/convert_themes.py create mode 100644 scratch/test_command_palette.py create mode 100644 scratch/test_multifile.py create mode 100644 scratch/test_textual_theme.py create mode 100644 scratch/test_tui_theme.py create mode 100644 src/tui/theme_manager.py create mode 100644 src/tui/themes/adw.json create mode 100644 src/tui/themes/atuel.json create mode 100644 src/tui/themes/ayu-blue.json create mode 100644 src/tui/themes/ayu-green.json create mode 100644 src/tui/themes/ayu-red.json create mode 100644 src/tui/themes/breeze.json create mode 100644 src/tui/themes/catppuccin-frappe-blue.json create mode 100644 src/tui/themes/catppuccin-frappe-lavender.json create mode 100644 src/tui/themes/catppuccin-frappe-mauve.json create mode 100644 src/tui/themes/catppuccin-frappe-pink.json create mode 100644 src/tui/themes/catppuccin-frappe-rosewater.json create mode 100644 src/tui/themes/catppuccin-frappe-sapphirejson.json create mode 100644 src/tui/themes/catppuccin-lavender.json create mode 100644 src/tui/themes/catppuccin-macchiato-lavender.json create mode 100644 src/tui/themes/catppuccin-macchiato-mauve.json create mode 100644 src/tui/themes/catppuccin-macchiato-pink.json create mode 100644 src/tui/themes/catppuccin-macchiato-rosewater.json create mode 100644 src/tui/themes/catppuccin-macchiato-sapphire.json create mode 100644 src/tui/themes/catppuccin-mocha-pink.json create mode 100644 src/tui/themes/catppuccin-mocha-rosewater.json create mode 100644 src/tui/themes/catppuccin-mocha-sapphire.json create mode 100644 src/tui/themes/catppuccin.json create mode 100644 src/tui/themes/cherry-blossom.json create mode 100644 src/tui/themes/cream-autumn.json create mode 100644 src/tui/themes/cream.json create mode 100644 src/tui/themes/creamy-forest.json create mode 100644 src/tui/themes/cyberpunk.json create mode 100644 src/tui/themes/default.json create mode 100644 src/tui/themes/doomed.json create mode 100644 src/tui/themes/dracula.json create mode 100644 src/tui/themes/espresso-cream.json create mode 100644 src/tui/themes/everdeer.json create mode 100644 src/tui/themes/everforest-alt.json create mode 100644 src/tui/themes/everforest-material.json create mode 100644 src/tui/themes/everforest.json create mode 100644 src/tui/themes/flexoki.json create mode 100644 src/tui/themes/garnet.json create mode 100644 src/tui/themes/github-dark.json create mode 100644 src/tui/themes/gruber-darker.json create mode 100644 src/tui/themes/gruvbox-material.json create mode 100644 src/tui/themes/gruvbox.json create mode 100644 src/tui/themes/gruvboxalt.json create mode 100644 src/tui/themes/hexa34c.json create mode 100644 src/tui/themes/horizon.json create mode 100644 src/tui/themes/jiva-lotus.json create mode 100644 src/tui/themes/jiva-paper.json create mode 100644 src/tui/themes/jiva.json create mode 100644 src/tui/themes/kanagawa-dragon.json create mode 100644 src/tui/themes/kanagawa-kasumi.json create mode 100644 src/tui/themes/kanagawa-paper.json create mode 100644 src/tui/themes/kemuri-koke.json create mode 100644 src/tui/themes/kemuri-susu.json create mode 100644 src/tui/themes/kemuri.json create mode 100644 src/tui/themes/lilac-amoled.json create mode 100644 src/tui/themes/macaron.json create mode 100644 src/tui/themes/matecito.json create mode 100644 src/tui/themes/miasma.json create mode 100644 src/tui/themes/mine.json create mode 100644 src/tui/themes/mizuki-akiyama.json create mode 100644 src/tui/themes/monochrome.json create mode 100644 src/tui/themes/murasaki.json create mode 100644 src/tui/themes/murata.json create mode 100644 src/tui/themes/naysayer.json create mode 100644 src/tui/themes/neon-surf.json create mode 100644 src/tui/themes/noctalia-legacy.json create mode 100644 src/tui/themes/nord-aurora.json create mode 100644 src/tui/themes/nord.json create mode 100644 src/tui/themes/oasis-abyss.json create mode 100644 src/tui/themes/occult-umbral.json create mode 100644 src/tui/themes/one-dark-two.json create mode 100644 src/tui/themes/one.json create mode 100644 src/tui/themes/osaka-jade.json create mode 100644 src/tui/themes/oxide.json create mode 100644 src/tui/themes/oxocarbon.json create mode 100644 src/tui/themes/paradise.json create mode 100644 src/tui/themes/peche.json create mode 100644 src/tui/themes/rose-pine-alt.json create mode 100644 src/tui/themes/rose-pine-moon-alt.json create mode 100644 src/tui/themes/rose-pine-moon.json create mode 100644 src/tui/themes/rosey-amoled.json create mode 100644 src/tui/themes/shien.json create mode 100644 src/tui/themes/shinonome.json create mode 100644 src/tui/themes/solarized-osaka.json create mode 100644 src/tui/themes/solarized.json create mode 100644 src/tui/themes/sway-classic.json create mode 100644 src/tui/themes/tokyo-night-moon.json create mode 100644 src/tui/themes/tokyo-night-storm.json create mode 100644 src/tui/themes/tokyo-night.json create mode 100644 src/tui/themes/vesper.json create mode 100644 src/tui/themes/zenbones.json diff --git a/config.json b/config.json index 14e1b35..7775fb9 100644 --- a/config.json +++ b/config.json @@ -1,4 +1,5 @@ { + "enable_boilerplate": false, "llm": { "base_url": "http://100.82.205.18:1010/", "model": "local-model", diff --git a/config.json.local b/config.json.local index ae206e2..7a99f12 100644 --- a/config.json.local +++ b/config.json.local @@ -1,4 +1,5 @@ { + "enable_boilerplate": false, "llm": { "base_url": "http://127.0.0.1:1010", "model": "local-model", diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index b90c2fe..481d41b 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -43,19 +43,38 @@ export interface HandbookCatalog { subjects: HandbookTopic[]; } +export const getConfig = async (): Promise<{ enable_boilerplate: boolean }> => { + try { + const response = await apiClient.get('/config'); + return response.data; + } catch (e) { + return { enable_boilerplate: false }; + } +}; + export const getChallenges = async (): Promise => { const response = await apiClient.get('/challenges'); return response.data.challenges; }; -export const runCode = async (language: string, code: string, stdin: string = '') => { - const response = await apiClient.post('/run', { language, code, stdin }); +export const runCode = async ( + language: string, + code: string, + stdin: string = '', + files?: Record, + activeFile?: string +) => { + const response = await apiClient.post('/run', { language, code, stdin, files, active_file: activeFile }); return response.data; }; - -export const lintCode = async (language: string, code: string) => { - const response = await apiClient.post('/lint', { language, code }); +export const lintCode = async ( + language: string, + code: string, + files?: Record, + activeFile?: string +) => { + const response = await apiClient.post('/lint', { language, code, files, active_file: activeFile }); return response.data; }; @@ -63,13 +82,17 @@ export const getGuidance = async ( challengeId: string, language: string, code: string, - question: string = '' + question: string = '', + files?: Record, + activeFile?: string ) => { const response = await apiClient.post('/guide', { challenge_id: challengeId, language, code, question, + files, + active_file: activeFile, }); return response.data; }; diff --git a/frontend/src/pages/Workspace.tsx b/frontend/src/pages/Workspace.tsx index a614e85..648f16a 100644 --- a/frontend/src/pages/Workspace.tsx +++ b/frontend/src/pages/Workspace.tsx @@ -8,6 +8,7 @@ import { getGuidance, getHandbookCatalog, getHandbookExample, + getConfig, Challenge, HandbookTopic, HandbookCatalog, @@ -32,18 +33,128 @@ import { Terminal, GripVertical, GripHorizontal, + Folder, + FolderPlus, + Plus, + Edit2, + Trash2, + FileCode, } from 'lucide-react'; interface WorkspaceProps { - challengeId: string; + challengeId?: string; onBack: () => void; } +function getLanguageFromFilename(filename: string): string { + const ext = filename.split('.').pop()?.toLowerCase() || ''; + const map: Record = { + 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 { + 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 \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': '\n\n\n App\n\n\n

Hello World

\n\n\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 = ({ challengeId, onBack }) => { const [allChallenges, setAllChallenges] = useState([]); const [currentChallenge, setCurrentChallenge] = useState(null); + const [enableBoilerplate, setEnableBoilerplate] = useState(false); + + // Multi-file & Folder state + const [files, setFiles] = useState>({ 'main.py': '' }); + const [folders, setFolders] = useState([]); + const [activeFile, setActiveFile] = useState('main.py'); + const [openTabs, setOpenTabs] = useState(['main.py']); + + // Modals for File & Folder Operations + const [isNewFileModalOpen, setIsNewFileModalOpen] = useState(false); + const [newFileNameInput, setNewFileNameInput] = useState(''); + const [isNewFolderModalOpen, setIsNewFolderModalOpen] = useState(false); + const [newFolderNameInput, setNewFolderNameInput] = useState(''); + const [isRenameModalOpen, setIsRenameModalOpen] = useState(false); + const [renameFileNameInput, setRenameFileNameInput] = useState(''); - const [code, setCode] = useState(''); const [stdinText, setStdinText] = useState(''); // Resizable Layout Dimensions @@ -59,8 +170,8 @@ const Workspace: React.FC = ({ challengeId, onBack }) => { text: 'Execution and lint output will appear here...', }); - // Right sidebar state: 'mentor' | 'handbook' | 'closed' - const [rightSidebarMode, setRightSidebarMode] = useState<'mentor' | 'handbook' | 'closed'>('mentor'); + // Right sidebar state: 'mentor' | 'handbook' | 'files' | 'closed' + const [rightSidebarMode, setRightSidebarMode] = useState<'mentor' | 'handbook' | 'files' | 'closed'>('mentor'); // Mentor state const [guidance, setGuidance] = useState('Ask the mentor a question or click Check Answer below...'); @@ -80,6 +191,15 @@ const Workspace: React.FC = ({ challengeId, onBack }) => { const [isRunning, setIsRunning] = useState(false); const [isLinting, setIsLinting] = useState(false); + // Load backend config on mount + useEffect(() => { + getConfig() + .then((cfg) => { + setEnableBoilerplate(cfg.enable_boilerplate); + }) + .catch(() => {}); + }, []); + // Load all challenges useEffect(() => { getChallenges() @@ -95,7 +215,7 @@ const Workspace: React.FC = ({ challengeId, onBack }) => { .catch((err) => console.error('Failed to load challenges:', err)); }, [challengeId]); - // Fetch handbook catalog when current challenge language changes + // Load handbook catalog when current challenge language changes useEffect(() => { if (currentChallenge) { getHandbookCatalog(currentChallenge.language) @@ -105,8 +225,45 @@ const Workspace: React.FC = ({ challengeId, onBack }) => { setHandbookExample(''); }) .catch((err) => console.error('Failed to load handbook catalog:', err)); + + const defaultFiles = getDefaultFilesForLanguage(currentChallenge.language, enableBoilerplate); + setFiles(defaultFiles); + const keys = Object.keys(defaultFiles); + const first = keys[0] || 'main.py'; + setActiveFile(first); + setOpenTabs(keys); } - }, [currentChallenge]); + }, [currentChallenge, enableBoilerplate]); + + const handleCreateFolder = (folderName: string) => { + const name = folderName.trim(); + if (!name) return; + if (!folders.includes(name)) { + setFolders((prev) => [...prev, name]); + } + setIsNewFolderModalOpen(false); + setNewFolderNameInput(''); + }; + + // Global keyboard shortcuts (Ctrl+1..9 for tabs, Ctrl+T for new file) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 't') { + e.preventDefault(); + setNewFileNameInput(''); + setIsNewFileModalOpen(true); + } + if ((e.ctrlKey || e.metaKey) && e.key >= '1' && e.key <= '9') { + const idx = parseInt(e.key, 10) - 1; + if (idx < openTabs.length) { + e.preventDefault(); + setActiveFile(openTabs[idx]); + } + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [openTabs]); // Global mousemove and mouseup listeners for drag-to-resize splitters useEffect(() => { @@ -177,14 +334,76 @@ const Workspace: React.FC = ({ challengeId, onBack }) => { } }; + // Multi-file tab & file operation handlers + const handleCreateFile = (filename: string) => { + const name = filename.trim(); + if (!name) return; + if (files[name] !== undefined) { + if (!openTabs.includes(name)) { + setOpenTabs((prev) => [...prev, name]); + } + setActiveFile(name); + } else { + setFiles((prev) => ({ ...prev, [name]: '' })); + setOpenTabs((prev) => [...prev, name]); + setActiveFile(name); + } + setIsNewFileModalOpen(false); + setNewFileNameInput(''); + }; + + const handleCloseTab = (tabName: string, e?: React.MouseEvent) => { + if (e) { + e.stopPropagation(); + } + if (openTabs.length <= 1) return; + const nextTabs = openTabs.filter((t) => t !== tabName); + setOpenTabs(nextTabs); + if (activeFile === tabName) { + setActiveFile(nextTabs[nextTabs.length - 1]); + } + }; + + const handleRenameActiveFile = (newName: string) => { + const name = newName.trim(); + if (!name || name === activeFile) { + setIsRenameModalOpen(false); + return; + } + const content = files[activeFile] || ''; + const newFiles = { ...files }; + delete newFiles[activeFile]; + newFiles[name] = content; + + setFiles(newFiles); + setOpenTabs((prev) => prev.map((t) => (t === activeFile ? name : t))); + setActiveFile(name); + setIsRenameModalOpen(false); + setRenameFileNameInput(''); + }; + + const handleDeleteFile = (fileName: string) => { + if (Object.keys(files).length <= 1) return; + const newFiles = { ...files }; + delete newFiles[fileName]; + setFiles(newFiles); + const nextTabs = openTabs.filter((t) => t !== fileName); + setOpenTabs(nextTabs.length > 0 ? nextTabs : [Object.keys(newFiles)[0]]); + if (activeFile === fileName) { + setActiveFile(Object.keys(newFiles)[0]); + } + }; + const handleRunCode = async (overrideStdin?: string) => { - if (!code.trim() || !currentChallenge) return; + const currentCode = files[activeFile] || ''; + if (!currentCode.trim() || !currentChallenge) return; setIsRunning(true); setOutput({ status: 'idle', text: 'Executing code...' }); const inputToSend = overrideStdin !== undefined ? overrideStdin : stdinText; try { - const res = await runCode(getMonacoLanguage(currentChallenge.language), code, inputToSend); + const lang = getLanguageFromFilename(activeFile); + const res = await runCode(lang, currentCode, inputToSend, files, activeFile); if (res.exit_code === 0) { setOutput({ status: 'success', @@ -206,11 +425,13 @@ const Workspace: React.FC = ({ challengeId, onBack }) => { }; const handleLintCode = async () => { - if (!code.trim() || !currentChallenge) return; + const currentCode = files[activeFile] || ''; + if (!currentCode.trim() || !currentChallenge) return; setIsLinting(true); setOutput({ status: 'idle', text: 'Running syntax & style lint...' }); try { - const res = await lintCode(getMonacoLanguage(currentChallenge.language), code); + const lang = getLanguageFromFilename(activeFile); + const res = await lintCode(lang, currentCode, files, activeFile); if (res.exit_code === 0) { setOutput({ status: 'success', text: '✓ Syntax & Style clean! No linting errors detected.' }); } else { @@ -235,11 +456,14 @@ const Workspace: React.FC = ({ challengeId, onBack }) => { setUserQuestion(''); try { + const lang = getLanguageFromFilename(activeFile); const res = await getGuidance( cid, - getMonacoLanguage(currentChallenge.language), - code, - q + lang, + files[activeFile] || '', + q, + files, + activeFile ); const qHeader = q ? `### Question / Evaluation:\n> ${q}\n\n---\n\n` : ''; setGuidance(`${qHeader}${res.mentor_response || 'No guidance received.'}`); @@ -343,7 +567,7 @@ const Workspace: React.FC = ({ challengeId, onBack }) => {
- {/* Dual Sidebar Toggles */} + {/* Sidebar Toggles: File Browser (Folder icon), Mentor, Syntax Handbook */} + + + )} +
+ ))} + + + +
+ {getLanguageFromFilename(activeFile)} +
{/* Code Editor */}
{ editorRef.current = editor; registerMonacoCompletions(monaco); }} - onChange={(val) => setCode(val || '')} + onChange={(val) => + setFiles((prev) => ({ ...prev, [activeFile]: val || '' })) + } options={{ minimap: { enabled: false }, fontSize: 13, @@ -611,7 +898,111 @@ const Workspace: React.FC = ({ challengeId, onBack }) => {
)} - {/* Right Column Mode 1: Mentor Sidebar (Scalable Width) */} + {/* Right Column Mode 1: File Browser Sidebar */} + {rightSidebarMode === 'files' && ( +
+
+

+ File Browser +

+
+ + + +
+
+ +
+ {folders.map((folderName) => ( +
+
+ + {folderName}/ +
+
+ ))} + + {Object.keys(files).map((fileName) => ( +
{ + 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' + }`} + > +
+ + {fileName} +
+
+ + {Object.keys(files).length > 1 && ( + + )} +
+
+ ))} +
+
+ )} + + {/* Right Column Mode 2: Mentor Sidebar (Scalable Width) */} {rightSidebarMode === 'mentor' && (
= ({ challengeId, onBack }) => {
)} - {/* Right Column Mode 2: Handbook Sidebar (Scalable Width) */} + {/* Right Column Mode 3: Handbook Sidebar (Scalable Width) */} {rightSidebarMode === 'handbook' && (
= ({ challengeId, onBack }) => {
- {/* Topic Catalog List (Fills full height when no topic selected) */} + {/* Topic Catalog List */}
@@ -788,7 +1179,7 @@ const Workspace: React.FC = ({ challengeId, onBack }) => { )}
- {/* Generated LLM Reference Card View (ONLY renders after selecting a topic) */} + {/* Generated LLM Reference Card View */} {selectedTopic && (
@@ -808,6 +1199,117 @@ const Workspace: React.FC = ({ challengeId, onBack }) => {
)}
+ + {/* New File Modal */} + {isNewFileModalOpen && ( +
+
+

+ Create New File +

+ 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" + /> +
+ + +
+
+
+ )} + + {/* New Folder Modal */} + {isNewFolderModalOpen && ( +
+
+

+ Create New Folder +

+ 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" + /> +
+ + +
+
+
+ )} + + {/* Rename File Modal */} + {isRenameModalOpen && ( +
+
+

+ Rename File +

+ 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" + /> +
+ + +
+
+
+ )} ); }; diff --git a/scratch/convert_themes.py b/scratch/convert_themes.py new file mode 100644 index 0000000..a325dcd --- /dev/null +++ b/scratch/convert_themes.py @@ -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}/!") diff --git a/scratch/test_command_palette.py b/scratch/test_command_palette.py new file mode 100644 index 0000000..386c220 --- /dev/null +++ b/scratch/test_command_palette.py @@ -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()) diff --git a/scratch/test_multifile.py b/scratch/test_multifile.py new file mode 100644 index 0000000..3b1ceaf --- /dev/null +++ b/scratch/test_multifile.py @@ -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 \nint main() { printMessage(); return 0; }\n', + "main.h": '#ifndef MAIN_H\n#define MAIN_H\n#include \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() diff --git a/scratch/test_textual_theme.py b/scratch/test_textual_theme.py new file mode 100644 index 0000000..4daf563 --- /dev/null +++ b/scratch/test_textual_theme.py @@ -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) diff --git a/scratch/test_tui_theme.py b/scratch/test_tui_theme.py new file mode 100644 index 0000000..4ea37d2 --- /dev/null +++ b/scratch/test_tui_theme.py @@ -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()) diff --git a/src/api/main.py b/src/api/main.py index c02b032..f03d6af 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -6,6 +6,7 @@ from src.core.loader import ChallengeLoader from src.core.executor import executor from src.core.linter import linter from src.core.prompts import get_socratic_prompt +from src.core.config import config app = FastAPI(title="Socratic Tutor API") @@ -20,25 +21,40 @@ app.add_middleware( loader = ChallengeLoader() +@app.get("/config") +def get_app_config(): + return { + "enable_boilerplate": config.enable_boilerplate, + "llm_base_url": config.llm_base_url, + "llm_model": config.llm_model, + } + + +from typing import Optional, Dict + + class LintRequest(BaseModel): language: str - code: str - - -from typing import Optional + code: Optional[str] = "" + files: Optional[Dict[str, str]] = None + active_file: Optional[str] = None class GuideRequest(BaseModel): challenge_id: str language: str - code: str + code: Optional[str] = "" question: Optional[str] = "" + files: Optional[Dict[str, str]] = None + active_file: Optional[str] = None class RunRequest(BaseModel): language: str - code: str + code: Optional[str] = "" stdin: Optional[str] = "" + files: Optional[Dict[str, str]] = None + active_file: Optional[str] = None @app.get("/challenges") @@ -60,7 +76,7 @@ def get_challenge(challenge_id: str): @app.post("/lint") def lint_code(req: LintRequest): """Runs linting on user code using Core Linter.""" - res = linter.lint(req.language, req.code) + res = linter.lint(req.language, req.code or "", files=req.files, active_file=req.active_file) if res.get("stderr") == f"Language '{req.language}' is not supported.": raise HTTPException(status_code=404, detail=res["stderr"]) return res @@ -69,7 +85,7 @@ def lint_code(req: LintRequest): @app.post("/run") def run_code(req: RunRequest): """Executes user code using Core Executor.""" - res = executor.run(req.language, req.code, stdin=req.stdin or "") + res = executor.run(req.language, req.code or "", stdin=req.stdin or "", files=req.files) if res.get("stderr") == f"Language '{req.language}' is not supported.": raise HTTPException(status_code=404, detail=res["stderr"]) return res @@ -100,7 +116,11 @@ async def guide_code(req: GuideRequest): challenge = challenges[req.challenge_id] guidance = await mentor.get_guidance_async( - challenge, req.code, user_question=req.question or "" + challenge, + user_code=req.code or "", + user_question=req.question or "", + files=req.files, + active_file=req.active_file, ) return { diff --git a/src/core/config.py b/src/core/config.py index b4709f7..ef9323f 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -8,6 +8,7 @@ class Config: """Manages application configuration settings.""" DEFAULT_CONFIG = { + "enable_boilerplate": False, "llm": { "base_url": "http://localhost:8080/v1", "model": "local-model", @@ -34,6 +35,8 @@ class Config: try: with open(self.config_path, "r", encoding="utf-8") as f: user_config = json.load(f) + if "enable_boilerplate" in user_config: + config["enable_boilerplate"] = bool(user_config["enable_boilerplate"]) if "llm" in user_config and isinstance(user_config["llm"], dict): config["llm"].update(user_config["llm"]) if "web" in user_config and isinstance(user_config["web"], dict): @@ -50,8 +53,11 @@ class Config: except Exception as e: print(f"Warning: Failed to create default {self.config_path}: {e}") - # Allow environment variable overrides + env_boilerplate = os.getenv("TACTTERM_ENABLE_BOILERPLATE") + if env_boilerplate is not None: + config["enable_boilerplate"] = env_boilerplate.lower() in ("true", "1", "yes") + env_base_url = os.getenv("TACTTERM_LLM_BASE_URL") if env_base_url: config["llm"]["base_url"] = env_base_url @@ -88,6 +94,10 @@ class Config: return config + @property + def enable_boilerplate(self) -> bool: + return bool(self._data.get("enable_boilerplate", False)) + @property def llm_base_url(self) -> str: return self._data["llm"]["base_url"].rstrip("/") diff --git a/src/core/executor.py b/src/core/executor.py index 248e99c..4a8ea63 100644 --- a/src/core/executor.py +++ b/src/core/executor.py @@ -8,9 +8,15 @@ from src.core.registry import registry class CodeExecutor: - """Executes code for all 10 supported programming languages with stdin support.""" + """Executes code for all 10 supported programming languages with stdin support and multi-file projects.""" - def run(self, language: str, code: str, stdin: str = "") -> Dict[str, Any]: + def run( + self, + language: str, + code: str = "", + stdin: str = "", + files: Dict[str, str] = None, + ) -> Dict[str, Any]: config = registry.get_config(language) if not config: return { @@ -24,20 +30,42 @@ class CodeExecutor: ext = config.get("ext", ".txt") with tempfile.TemporaryDirectory() as tmpdir: - if lang_key == "java": - temp_file_name = "Main.java" - elif lang_key == "cpp": - temp_file_name = "main.cpp" - elif lang_key == "rust": - temp_file_name = "main.rs" - elif lang_key == "csharp": - temp_file_name = "Program.cs" + # 1. Write multi-file project contents or fallback single file + if files and isinstance(files, dict) and len(files) > 0: + for rel_path, content in files.items(): + target_path = os.path.join(tmpdir, rel_path) + os.makedirs(os.path.dirname(target_path), exist_ok=True) + with open(target_path, "w", encoding="utf-8") as f: + f.write(content) + # Determine main entry file + main_file_candidates = [ + "main.cpp", "Main.java", "main.rs", "Program.cs", "main.py", + "main.go", "main.js", "main.ts", "main.lua", "index.html" + ] + temp_file_name = None + for candidate in main_file_candidates: + if candidate in files: + temp_file_name = candidate + break + if not temp_file_name: + temp_file_name = list(files.keys())[0] else: - temp_file_name = f"main{ext}" + if lang_key == "java": + temp_file_name = "Main.java" + elif lang_key == "cpp": + temp_file_name = "main.cpp" + elif lang_key == "rust": + temp_file_name = "main.rs" + elif lang_key == "csharp": + temp_file_name = "Program.cs" + else: + temp_file_name = f"main{ext}" + + temp_file_path = os.path.join(tmpdir, temp_file_name) + with open(temp_file_path, "w", encoding="utf-8") as f: + f.write(code) temp_file_path = os.path.join(tmpdir, temp_file_name) - with open(temp_file_path, "w", encoding="utf-8") as f: - f.write(code) try: if lang_key == "python": @@ -51,10 +79,13 @@ class CodeExecutor: timeout=15, ) elif lang_key == "html": + primary_code = code + if files and temp_file_name in files: + primary_code = files[temp_file_name] return { "language": language, "exit_code": 0, - "stdout": code, + "stdout": primary_code, "stderr": "", } elif lang_key == "javascript": @@ -77,7 +108,7 @@ class CodeExecutor: ) elif lang_key == "go": result = subprocess.run( - ["go", "run", temp_file_path], + ["go", "run", "."], input=stdin, capture_output=True, text=True, @@ -94,8 +125,17 @@ class CodeExecutor: timeout=15, ) elif lang_key == "cpp": + # Collect all .cpp files recursively in tmpdir + cpp_files = [] + for root, _, fnames in os.walk(tmpdir): + for fn in fnames: + if fn.endswith(".cpp"): + cpp_files.append(os.path.relpath(os.path.join(root, fn), tmpdir)) + if not cpp_files: + cpp_files = [temp_file_name] + compile_res = subprocess.run( - ["g++", "-O2", temp_file_path, "-o", "main"], + ["g++", "-O2"] + cpp_files + ["-o", "main"], capture_output=True, text=True, cwd=tmpdir, @@ -140,8 +180,16 @@ class CodeExecutor: timeout=15, ) elif lang_key == "java": + java_files = [] + for root, _, fnames in os.walk(tmpdir): + for fn in fnames: + if fn.endswith(".java"): + java_files.append(os.path.relpath(os.path.join(root, fn), tmpdir)) + if not java_files: + java_files = [temp_file_name] + compile_res = subprocess.run( - ["javac", temp_file_path], + ["javac"] + java_files, capture_output=True, text=True, cwd=tmpdir, @@ -154,8 +202,9 @@ class CodeExecutor: "stdout": compile_res.stdout, "stderr": f"Javac Compilation Error:\n{compile_res.stderr}", } + main_class = os.path.splitext(temp_file_name)[0] result = subprocess.run( - ["java", "Main"], + ["java", main_class], input=stdin, capture_output=True, text=True, diff --git a/src/core/linter.py b/src/core/linter.py index 10b9ceb..ea779cd 100644 --- a/src/core/linter.py +++ b/src/core/linter.py @@ -10,7 +10,13 @@ from src.core.registry import registry class CodeLinter: """Lints user code for syntax and style issues for 10 languages.""" - def lint(self, language: str, code: str) -> Dict[str, Any]: + def lint( + self, + language: str, + code: str = "", + files: Dict[str, str] = None, + active_file: str = None, + ) -> Dict[str, Any]: config = registry.get_config(language) if not config: return { @@ -24,20 +30,30 @@ class CodeLinter: ext = config.get("ext", ".txt") with tempfile.TemporaryDirectory() as tmpdir: - if lang_key == "java": - temp_file_name = "Main.java" - elif lang_key == "cpp": - temp_file_name = "main.cpp" - elif lang_key == "rust": - temp_file_name = "main.rs" - elif lang_key == "csharp": - temp_file_name = "Program.cs" + if files and isinstance(files, dict) and len(files) > 0: + for rel_path, content in files.items(): + target_path = os.path.join(tmpdir, rel_path) + os.makedirs(os.path.dirname(target_path), exist_ok=True) + with open(target_path, "w", encoding="utf-8") as f: + f.write(content) + temp_file_name = active_file if active_file and active_file in files else list(files.keys())[0] else: - temp_file_name = f"main{ext}" + if lang_key == "java": + temp_file_name = "Main.java" + elif lang_key == "cpp": + temp_file_name = "main.cpp" + elif lang_key == "rust": + temp_file_name = "main.rs" + elif lang_key == "csharp": + temp_file_name = "Program.cs" + else: + temp_file_name = f"main{ext}" + + temp_path = os.path.join(tmpdir, temp_file_name) + with open(temp_path, "w", encoding="utf-8") as f: + f.write(code) temp_path = os.path.join(tmpdir, temp_file_name) - with open(temp_path, "w", encoding="utf-8") as f: - f.write(code) try: if lang_key == "python": diff --git a/src/core/mentor.py b/src/core/mentor.py index c16a6f9..15f7c15 100644 --- a/src/core/mentor.py +++ b/src/core/mentor.py @@ -21,10 +21,17 @@ class MentorClient: self.config = config async def get_guidance_async( - self, challenge: object, user_code: str, user_question: str = "" + self, + challenge: object, + user_code: str = "", + user_question: str = "", + files: Optional[Dict[str, str]] = None, + active_file: Optional[str] = None, ) -> Dict[str, Any]: """Fetch mentor guidance asynchronously from the configured LLM endpoint.""" - prompt = get_mentor_prompt(challenge, user_code, user_question=user_question) + prompt = get_mentor_prompt( + challenge, user_code, user_question=user_question, files=files, active_file=active_file + ) endpoint = get_chat_completions_url(self.config.llm_base_url) user_content = ( @@ -92,7 +99,12 @@ class MentorClient: } def get_guidance( - self, challenge: object, user_code: str, user_question: str = "" + self, + challenge: object, + user_code: str = "", + user_question: str = "", + files: Optional[Dict[str, str]] = None, + active_file: Optional[str] = None, ) -> Dict[str, Any]: """Synchronous wrapper for fetching mentor guidance.""" import asyncio @@ -103,15 +115,21 @@ class MentorClient: import nest_asyncio nest_asyncio.apply() return loop.run_until_complete( - self.get_guidance_async(challenge, user_code, user_question=user_question) + self.get_guidance_async( + challenge, user_code, user_question=user_question, files=files, active_file=active_file + ) ) else: return asyncio.run( - self.get_guidance_async(challenge, user_code, user_question=user_question) + self.get_guidance_async( + challenge, user_code, user_question=user_question, files=files, active_file=active_file + ) ) except Exception: return asyncio.run( - self.get_guidance_async(challenge, user_code, user_question=user_question) + self.get_guidance_async( + challenge, user_code, user_question=user_question, files=files, active_file=active_file + ) ) diff --git a/src/core/prompts.py b/src/core/prompts.py index a57f174..1cb73a9 100644 --- a/src/core/prompts.py +++ b/src/core/prompts.py @@ -41,7 +41,81 @@ Your ultimate mission is to build the user's confidence in designing and constru SYSTEM_PROMPT_SOCRATIC = SYSTEM_PROMPT_MENTOR -def get_mentor_prompt(challenge: object, user_code: str, user_question: str = "") -> str: +from typing import Dict, Optional + + +def compress_file_content(filename: str, content: str, max_lines: int = 30) -> str: + """Compresses file content by extracting signatures and truncating bodies if context limit is reached.""" + lines = content.splitlines() + if len(lines) <= max_lines: + return content + + headers = [] + for line in lines: + stripped = line.strip() + if ( + stripped.startswith(("#include", "import ", "from ", "using ", "package ")) + or stripped.startswith(("def ", "class ", "struct ", "fn ", "interface ", "enum ", "public ", "private ")) + or stripped.endswith(("{", ":")) + ): + headers.append(line) + + if len(headers) >= 5: + return "\n".join(headers[:max_lines]) + f"\n... [{len(lines) - len(headers)} lines omitted]" + else: + return "\n".join(lines[:max_lines]) + f"\n... [{len(lines) - max_lines} lines omitted]" + + +def format_multifile_context( + user_code: str | Dict[str, str] = "", + files: Optional[Dict[str, str]] = None, + active_file: Optional[str] = None, + max_chars: int = 12000, +) -> str: + """Formats single or multi-file project code for the LLM mentor, applying context compression if needed.""" + if isinstance(user_code, dict) and files is None: + files = user_code + user_code = "" + + if not files or not isinstance(files, dict): + code_str = str(user_code or "") + return code_str if code_str.strip() else "# No code written yet" + + active_name = active_file if active_file and active_file in files else list(files.keys())[0] + + total_len = sum(len(c) for c in files.values()) + should_compress = total_len > max_chars + + parts = [] + file_list_summary = "Project Structure:\n" + "\n".join( + f"- {fname}{' (Active)' if fname == active_name else ''}" for fname in files.keys() + ) + parts.append(file_list_summary) + + # 1. Active file first (kept in full) + active_content = files[active_name] + parts.append(f"=== File: {active_name} (Active) ===\n{active_content}") + + # 2. Inactive files (compressed if overall context is large) + for fname, content in files.items(): + if fname == active_name: + continue + if should_compress and not fname.endswith((".h", ".hpp", ".d.ts")): + compressed = compress_file_content(fname, content) + parts.append(f"=== File: {fname} (Compressed) ===\n{compressed}") + else: + parts.append(f"=== File: {fname} ===\n{content}") + + return "\n\n".join(parts) + + +def get_mentor_prompt( + challenge: object, + user_code: str, + user_question: str = "", + files: Optional[Dict[str, str]] = None, + active_file: Optional[str] = None, +) -> str: """Returns the formatted system prompt for the LLM mentor.""" reqs = "\n".join(f"- {r}" for r in getattr(challenge, "requirements", [])) hints = "\n".join(f"- {h}" for h in getattr(challenge, "hints", [])) @@ -52,6 +126,10 @@ def get_mentor_prompt(challenge: object, user_code: str, user_question: str = "" else "How should I structure my program and overcome my current blocker for this challenge?" ) + formatted_code = format_multifile_context( + user_code=user_code, files=files, active_file=active_file + ) + return SYSTEM_PROMPT_MENTOR.format( challenge_name=getattr(challenge, "name", "Coding Challenge"), language=getattr(challenge, "language", "Python"), @@ -59,7 +137,7 @@ def get_mentor_prompt(challenge: object, user_code: str, user_question: str = "" challenge_description=getattr(challenge, "description", ""), requirements=reqs if reqs else "- Follow standard problem specifications", hints=hints if hints else "- Think through edge cases", - user_code=user_code if user_code.strip() else "# No code written yet", + user_code=formatted_code, user_question=q_text, ) diff --git a/src/tui/app.py b/src/tui/app.py index d522a2a..74e53c0 100644 --- a/src/tui/app.py +++ b/src/tui/app.py @@ -1,9 +1,10 @@ """TactiTerm TUI — Coding Tutor Terminal User Interface with Handbook & Expandable Output Terminal.""" import argparse import logging -from typing import List, Dict, Any +from typing import List, Dict, Any, Iterable -from textual.app import App, ComposeResult +from textual.app import App, ComposeResult, SystemCommand +from textual.screen import Screen from textual.containers import Container, Horizontal, VerticalScroll from textual.widgets import Header, Footer, Static, OptionList, Button, Markdown, TextArea, Input, LoadingIndicator from textual.widgets.option_list import Option @@ -18,20 +19,140 @@ from src.core.prompts import get_mentor_prompt from src.tui.widgets import CodeEditor, MentorInputTextArea, StdinInput, TUI_COMPLETIONS, get_tui_language +from src.core.config import config +from src.tui.theme_manager import theme_manager +HELP_MENU_MARKDOWN = """# ⌨️ TactiTerm Controls & Shortcuts + +### 🎯 Focus Navigation & Quick Access +- **Ctrl + J** — Focus Challenge Selection List +- **Ctrl + K** — Focus Text Input (Code Editor) +- **Ctrl + L** — Focus Active Sidebar (when open) + +### 📄 Script Tabs & Project Files +- **Ctrl + 1..9** — Switch directly to Tab 1..9 +- **Ctrl + T** — Create new script tab +- **Ctrl + R** — Rename active script tab +- **Ctrl + Alt + C** — Close active script tab +- **Ctrl + A / D** — Switch to Previous / Next tab +- **Ctrl + F** — Toggle File Browser Sidebar + +### 🎨 Themes & Customization +- **Ctrl + P** — Cycle Community Palette Themes (Catppuccin, Dracula, Nord, Tokyo Night, Gruvbox, Cyberpunk, Everforest) + +### 📁 File Browser (Folder View) +- **↑ / ↓ Arrow Keys** — Navigate files and folders +- **Enter** — Open file in editor or expand folder +- **Ctrl + N** — Create new folder +- **Ctrl + Up / Down** — Move item up / down in tree + +### ⚡ Execution & AI Mentor +- **Ctrl + Enter (or F5)** — Run code execution +- **F1** — Toggle Socratic AI Mentor +- **F2** — Evaluate & Check Answer +- **F3** — Open Syntax Handbook + +### 🌐 Editor & Focus +- **Ctrl + W** — Toggle soft word wrap +- **Ctrl + E / F4** — Expand / collapse output console +- **F6** — Switch panel focus +- **Escape** — Exit focus / Close popup +- **Ctrl + H** — Toggle this help menu +""" + + +def get_tui_default_files(language: str, enable_boilerplate: bool = False) -> Dict[str, str]: + norm = (language or "python").lower().strip() + if not enable_boilerplate: + if norm in ("cpp", "c++", "cxx"): + return {"main.cpp": ""} + elif norm == "java": + return {"Main.java": ""} + elif norm in ("csharp", "c#"): + return {"Program.cs": ""} + elif norm == "rust": + return {"main.rs": ""} + elif norm == "go": + return {"main.go": ""} + elif norm in ("javascript", "js"): + return {"main.js": ""} + elif norm in ("typescript", "ts"): + return {"main.ts": ""} + elif norm == "html": + return {"index.html": ""} + elif norm == "lua": + return {"main.lua": ""} + return {"main.py": ""} + + if norm in ("cpp", "c++", "cxx"): + return { + "main.cpp": '// Main implementation\n#include "main.h"\n#include \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', + } + elif 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' + } + elif norm in ("csharp", "c#"): + return { + "Program.cs": 'using System;\n\nclass Program {\n static void Main() {\n Console.WriteLine("Hello from C#!");\n }\n}\n' + } + elif norm == "rust": + return { + "main.rs": 'fn main() {\n println!("Hello from Rust!");\n}\n' + } + elif norm == "go": + return { + "main.go": 'package main\nimport "fmt"\n\nfunc main() {\n fmt.Println("Hello from Go!")\n}\n' + } + elif norm in ("javascript", "js"): + return { + "main.js": 'console.log("Hello from JavaScript!");\n' + } + elif norm in ("typescript", "ts"): + return { + "main.ts": 'console.log("Hello from TypeScript!");\n' + } + elif norm == "html": + return { + "index.html": '\n\n\n App\n\n\n

Hello World

\n\n\n' + } + elif norm == "lua": + return { + "main.lua": 'print("Hello from Lua!")\n' + } + return { + "main.py": '# Main script\nprint("Hello from Python!")\n' + } + + class TactiTermTUI(App): - """A coding tutor TUI with direct core service integration, Mentor, Handbook, and Expandable Stdin Output Terminal.""" + """A coding tutor TUI with direct core service integration, Mentor, Handbook, Multi-file Tabs & Folder Tree Sidebar.""" TITLE = "TactiTerm" CSS = """ Screen { layout: vertical; + border: none; + padding: 0; + margin: 0; + } + + Header { + border: none; + } + + Footer { + border: none; } #main-container { layout: horizontal; height: 1fr; width: 100%; + layers: default overlay; + position: relative; + border: none; } /* Left column: challenge list & details */ @@ -52,8 +173,32 @@ class TactiTermTUI(App): position: relative; } - /* Right columns: Mentor & Handbook sidebars (hidden by default) */ - #mentor-sidebar, #handbook-sidebar { + /* Tab Bar & Prompt Styling */ + #tab-bar-label { + height: 1; + margin-bottom: 0; + background: transparent; + padding: 0; + } + + #file-prompt-bar { + display: none; + height: 3; + layout: horizontal; + margin-bottom: 1; + } + + #editor-panel.prompt-active #file-prompt-bar { + display: block; + } + + #file-prompt-input { + width: 1fr; + margin-right: 1; + } + + /* Right columns: Mentor, Handbook & Folder sidebars (hidden by default) */ + #mentor-sidebar, #handbook-sidebar, #folder-sidebar { width: 32%; min-width: 26; height: 100%; @@ -61,7 +206,27 @@ class TactiTermTUI(App): display: none; } - /* Adjusted column widths when a sidebar is toggled open */ + #main-container.sidebar-folder-open #folder-sidebar { + display: block; + width: 30%; + } + + #folder-header { + height: 3; + background: $accent; + color: white; + content-align: center middle; + text-style: bold; + margin-bottom: 1; + } + + #folder_tree_list { + height: 1fr; + border: solid $secondary; + margin-bottom: 1; + } + + /* Adjusted column widths when any sidebar is toggled open */ #main-container.sidebar-open #challenge-panel { width: 26%; } @@ -127,7 +292,7 @@ class TactiTermTUI(App): #action-bar Button { height: 1; - min-width: 7; + min-width: 3; padding: 0 1; margin-right: 1; border: none; @@ -161,6 +326,74 @@ class TactiTermTUI(App): display: block; } + /* Centered Help Menu Popup Overlay (Ctrl+H) */ + #help-modal-popup { + display: none; + layer: overlay; + position: absolute; + align: center middle; + width: 72; + height: 24; + background: #1e1e1e; + border: heavy $primary; + padding: 1; + } + + #main-container.show-help #help-modal-popup { + display: block; + } + + /* Centered Theme Selection Popup Overlay (Ctrl+P) */ + #theme-modal-popup { + display: none; + layer: overlay; + position: absolute; + align: center middle; + width: 68; + height: 22; + background: #1e1e1e; + border: heavy $primary; + padding: 1; + } + + #main-container.show-theme-menu #theme-modal-popup { + display: block; + } + + #theme-title { + height: 1; + text-style: bold; + color: white; + background: $primary; + content-align: center middle; + margin-bottom: 1; + } + + #theme_list_popup { + height: 1fr; + border: none; + background: transparent; + scrollbar-size: 0 0; + } + + #help-title { + height: 1; + text-style: bold; + color: white; + background: $primary; + content-align: center middle; + margin-bottom: 1; + } + + #help-content-scroll { + height: 1fr; + overflow: auto; + } + + #help-content-scroll:focus { + border: solid $accent; + } + #completion-title { display: none; } @@ -266,28 +499,49 @@ class TactiTermTUI(App): padding: 0 1; text-style: bold; } - """ + """ + theme_manager.generate_css() BINDINGS = [ - Binding("ctrl+r", "run_code", "Run"), - Binding("ctrl+l", "lint_code", "Lint"), - Binding("ctrl+w", "toggle_word_wrap", "Word Wrap (Ctrl+W)"), - Binding("ctrl+e,f4", "toggle_expand_output", "Expand Output (Ctrl+E / F4)"), + Binding("ctrl+h", "toggle_help_menu", "Help (Ctrl+H)", show=True), + Binding("ctrl+p", "cycle_theme", "Theme (Ctrl+P)", show=True), + Binding("ctrl+f", "toggle_folder_sidebar", "Files (Ctrl+F)", show=True), + Binding("ctrl+j", "focus_challenges", "Focus List (Ctrl+J)", show=True), + Binding("ctrl+k", "focus_editor", "Focus Input (Ctrl+K)", show=True), + Binding("ctrl+l", "focus_sidebar", "Focus Sidebar (Ctrl+L)", show=True), + Binding("ctrl+w", "toggle_word_wrap", "Wrap (Ctrl+W)", show=True), + Binding("ctrl+q", "quit", "Quit (Ctrl+Q)", show=True), + Binding("ctrl+enter", "run_code", "Run", show=False), + Binding("ctrl+r", "prompt_rename_tab", "Rename Tab", show=False), + Binding("ctrl+t", "prompt_new_tab", "New Tab", show=False), + Binding("ctrl+a", "prev_tab", "Prev Tab", show=False), + Binding("ctrl+d", "next_tab", "Next Tab", show=False), + Binding("ctrl+alt+c", "close_tab", "Close Tab", show=False), + Binding("ctrl+n", "prompt_new_folder", "New Folder", show=False), + Binding("ctrl+1", "select_tab_1", "Tab 1", show=False), + Binding("ctrl+2", "select_tab_2", "Tab 2", show=False), + Binding("ctrl+3", "select_tab_3", "Tab 3", show=False), + Binding("ctrl+4", "select_tab_4", "Tab 4", show=False), + Binding("ctrl+5", "select_tab_5", "Tab 5", show=False), + Binding("ctrl+6", "select_tab_6", "Tab 6", show=False), + Binding("ctrl+7", "select_tab_7", "Tab 7", show=False), + Binding("ctrl+8", "select_tab_8", "Tab 8", show=False), + Binding("ctrl+9", "select_tab_9", "Tab 9", show=False), + Binding("ctrl+e,f4", "toggle_expand_output", "Expand Output", show=False), Binding("f2", "check_answer", "Check", show=False), Binding("f1", "toggle_mentor_sidebar", "Mentor", show=False), Binding("f3", "toggle_handbook_sidebar", "Handbook", show=False), - Binding("ctrl+f", "refresh_challenges", "Refresh"), Binding("alt+w", "scroll_output_up", "Output Up", show=False), Binding("alt+s", "scroll_output_down", "Output Down", show=False), - Binding("alt+left,alt+a", "scroll_left", "Scroll Left", show=False), - Binding("alt+right,alt+d", "scroll_right", "Scroll Right", show=False), - Binding("ctrl+up", "scroll_mentor_up", "Sidebar Up", show=False), - Binding("ctrl+down", "scroll_mentor_down", "Sidebar Down", show=False), + Binding("alt+left", "scroll_left", "Scroll Left", show=False), + Binding("alt+right", "scroll_right", "Scroll Right", show=False), + Binding("ctrl+up", "scroll_mentor_up_or_move", "Up", show=False), + Binding("ctrl+down", "scroll_mentor_down_or_move", "Down", show=False), + Binding("ctrl+left", "move_folder_left", "Move Left", show=False), + Binding("ctrl+right", "move_folder_right", "Move Right", show=False), Binding("alt+up", "scroll_details_up", "Details Up", show=False), Binding("alt+down", "scroll_details_down", "Details Down", show=False), Binding("escape", "unfocus_editor", "Exit Focus"), - Binding("f6", "toggle_panel_focus", "Switch Focus"), - Binding("ctrl+q", "quit", "Quit"), + Binding("f6", "toggle_panel_focus", "Switch Focus", show=False), ] def __init__(self, debug: bool = False) -> None: @@ -301,6 +555,14 @@ class TactiTermTUI(App): self.active_completion_prefix = "" self.debug_mode = debug + # Multi-file & Folder state + self.files: Dict[str, str] = get_tui_default_files("python", enable_boilerplate=config.enable_boilerplate) + self.open_tabs: List[str] = list(self.files.keys()) + self.active_tab_index: int = 0 + self.folders: List[str] = [] + self.folder_tree_items: List[Dict[str, Any]] = [] + self.file_modal_mode: str = "" # "new_file", "rename_file", "new_folder" + def compose(self) -> ComposeResult: yield Header() @@ -312,16 +574,22 @@ class TactiTermTUI(App): with VerticalScroll(id="challenge-details-container"): yield Markdown("Select a challenge from the list above to view details...", id="challenge-details") - # Middle column: Code Editor, Completion Dropdown Popup & Expandable Output Terminal + # Middle column: Action Bar, Multi-File Tabs, Code Editor, Completion Dropdown Popup & Expandable Output Terminal with Container(id="editor-panel"): with Horizontal(id="action-bar"): - yield Button("▶ Run (Ctrl+R)", id="btn-run", variant="success") - yield Button("🔍 Lint (Ctrl+L)", id="btn-lint", variant="primary") + yield Button("▶ Run (Ctrl+Enter)", id="btn-run", variant="success") + yield Button("🔍 Lint", id="btn-lint", variant="primary") yield Button("✅ Check (F2)", id="btn-check", variant="success") yield Button("💡 Mentor (F1)", id="btn-guide", variant="warning") yield Button("📖 Handbook (F3)", id="btn-handbook", variant="primary") - yield Button("🌐 Wrap (Ctrl+W)", id="btn-wrap", variant="default") - yield Button("🔄 Refresh", id="btn-refresh", variant="default") + yield Button("📁", id="btn-files", variant="default") + + with Horizontal(id="file-prompt-bar"): + yield Input(placeholder="Enter name...", id="file-prompt-input") + yield Button("Confirm", id="btn-file-prompt-confirm", variant="success") + yield Button("Cancel", id="btn-file-prompt-cancel", variant="default") + + yield Static("", id="tab-bar-label") yield CodeEditor(id="editor") @@ -332,7 +600,7 @@ class TactiTermTUI(App): with Horizontal(id="output-header-bar"): yield Static("Output Console (Alt+W/S: Scroll)", id="output-title") - yield Button("⤢ Expand / Input (Ctrl+E)", id="btn-expand-output", variant="primary") + yield Button("⤢ Console (Ctrl+E)", id="btn-expand-output", variant="primary") with VerticalScroll(id="output-scroll-container"): yield Static("Execution and lint output will appear here...", id="output-container") @@ -361,6 +629,22 @@ class TactiTermTUI(App): with VerticalScroll(id="handbook-example-scroll"): yield Markdown("Select a function or topic above to view reference card...", id="handbook-example-display") + # Right column 3: Folder Tree Sidebar (Right hand side, hidden by default, toggled via Ctrl+F) + with Container(id="folder-sidebar"): + yield Static("📁 File Browser", id="folder-header") + yield OptionList(id="folder_tree_list") + + # Centered Help Menu Popup Overlay (Ctrl+H) + with Container(id="help-modal-popup"): + yield Static("⌨️ TactiTerm Keybindings & Controls (Esc / Ctrl+H to Close)", id="help-title") + with VerticalScroll(id="help-content-scroll"): + yield Markdown(HELP_MENU_MARKDOWN, id="help-markdown") + + # Centered Theme Selection Popup Overlay (Ctrl+P) + with Container(id="theme-modal-popup"): + yield Static("🎨 Select Theme Palette (↑/↓ to Navigate & Preview, Enter to Select, Esc to Close)", id="theme-title") + yield OptionList(id="theme_list_popup") + yield Static("Ready", id="status-bar") yield Footer() @@ -371,8 +655,41 @@ class TactiTermTUI(App): self.toggle_expand_output() def on_mount(self) -> None: + theme_manager.register_all(self) self.call_after_refresh(self.load_challenges) + def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]: + yield SystemCommand( + "Keys", + "Show TactiTerm controls and keyboard shortcuts", + self.action_toggle_help_menu, + ) + yield SystemCommand( + "Themes", + "Change the current palette theme", + self.action_change_theme, + ) + if screen.maximized is not None: + yield SystemCommand( + "Minimize", + "Minimize the widget and restore to normal size", + screen.action_minimize, + ) + elif screen.focused is not None and getattr(screen.focused, "allow_maximize", False): + yield SystemCommand( + "Maximize", "Maximize the focused widget", screen.action_maximize + ) + yield SystemCommand( + "Screenshot", + "Save an SVG screenshot of the current screen", + lambda: self.set_timer(0.1, self.deliver_screenshot), + ) + yield SystemCommand( + "Quit", + "Quit TactiTerm", + self.action_quit, + ) + def load_challenges(self) -> None: self.challenges_dict = self.loader.load_all() @@ -442,6 +759,8 @@ class TactiTermTUI(App): def on_input_submitted(self, event: Input.Submitted) -> None: if event.input.id == "program-stdin-input": self.send_program_stdin() + elif event.input.id == "file-prompt-input": + self.submit_file_prompt() def on_button_pressed(self, event: Button.Pressed) -> None: button_id = event.button.id @@ -451,6 +770,12 @@ class TactiTermTUI(App): self.action_lint_code() elif button_id == "btn-check": self.action_check_answer() + elif button_id == "btn-files": + self.action_toggle_folder_sidebar() + elif button_id == "btn-theme": + self.action_cycle_theme() + elif button_id == "btn-help": + self.action_toggle_help_menu() elif button_id == "btn-guide": self.action_toggle_mentor_sidebar() elif button_id == "btn-handbook": @@ -467,6 +792,340 @@ class TactiTermTUI(App): self.submit_mentor_question() elif button_id == "btn-refresh": self.action_refresh_challenges() + elif button_id == "btn-file-prompt-confirm": + self.submit_file_prompt() + elif button_id == "btn-file-prompt-cancel": + panel = self.query_one("#editor-panel") + panel.remove_class("prompt-active") + self.query_one("#editor", CodeEditor).focus() + + # ── Multi-File Tabs & Folder Tree Actions ───────────────────────── + + def update_tab_bar_display(self) -> None: + """Update tab bar header with browser-styled tab badges right above editor.""" + if not self.open_tabs: + return + tabs_str = [] + for idx, tab_name in enumerate(self.open_tabs): + is_active = (idx == self.active_tab_index) + prefix = f"^{idx+1}" + active_marker = "*" if is_active else "" + if is_active: + tabs_str.append(f"[bold white on #1e40af] {prefix} {tab_name}{active_marker} [/bold white on #1e40af]") + else: + tabs_str.append(f"[dim white on #2d2d2d] {prefix} {tab_name} [/dim white on #2d2d2d]") + + formatted = " ".join(tabs_str) + try: + self.query_one("#tab-bar-label", Static).update(formatted) + except Exception: + pass + + def action_toggle_help_menu(self) -> None: + """Toggle Help Menu Popup overlay (Ctrl+H). Focuses scroll container so ↑/↓ scroll content.""" + main_box = self.query_one("#main-container") + if main_box.has_class("show-help"): + main_box.remove_class("show-help") + self.query_one("#editor", CodeEditor).focus() + self.query_one("#status-bar", Static).update("Ready") + else: + main_box.add_class("show-help") + scroll_widget = self.query_one("#help-content-scroll", VerticalScroll) + scroll_widget.focus() + self.query_one("#status-bar", Static).update("⌨️ Help Menu open — Use ↑/↓ to scroll | Esc or Ctrl+H to close.") + + def action_cycle_theme(self) -> None: + """Open Theme Palette Selection Menu (Ctrl+P). Dynamically re-scans theme folders.""" + main_box = self.query_one("#main-container") + if main_box.has_class("show-theme-menu"): + main_box.remove_class("show-theme-menu") + self.query_one("#editor", CodeEditor).focus() + return + + theme_manager.load_all_themes() + self.refresh_theme_list_popup() + + main_box.add_class("show-theme-menu") + self.query_one("#theme_list_popup", OptionList).focus() + count = len(theme_manager.get_theme_ids()) + self.query_one("#status-bar", Static).update( + f"🎨 Theme Menu Open ({count} themes scanned) — Use ↑/↓ to preview, Enter to select, Esc to close." + ) + + def refresh_theme_list_popup(self) -> None: + theme_list = self.query_one("#theme_list_popup", OptionList) + theme_list.clear_options() + + # Top option: Refresh Themes + theme_list.add_option(Option("🔄 Refresh Theme List (Re-scan folder)", id="action:refresh")) + + all_themes = theme_manager.get_all_themes() + for t in all_themes: + theme_list.add_option(Option(f"🎨 {t['name']}", id=t["id"])) + + def apply_theme(self, theme_id_or_name: str) -> None: + target = str(theme_id_or_name).strip() + theme_info = theme_manager.get_theme(target) + if not theme_info or theme_info.get("id") not in theme_manager.themes: + for t in theme_manager.themes.values(): + if t["name"].lower() == target.lower(): + theme_info = t + break + + theme_id = theme_info.get("id", "default") + name = theme_info.get("name", theme_id) + + main_box = self.query_one("#main-container") + for t_id in theme_manager.get_theme_ids(): + main_box.remove_class(f"theme-{t_id}") + main_box.add_class(f"theme-{theme_id}") + + try: + if self.theme != name: + self.theme = name + except Exception: + pass + + self.query_one("#status-bar", Static).update(f"🎨 Palette theme set to: {name.upper()}") + + def on_theme_changed(self, event: Any) -> None: + new_theme = getattr(event, "theme", None) or getattr(self, "theme", "default") + self.apply_theme(str(new_theme)) + + def sync_editor_to_active_tab(self, save_current: bool = True) -> None: + """Syncs in-memory file content with CodeEditor widget.""" + editor = self.query_one("#editor", CodeEditor) + if save_current and self.open_tabs and self.active_tab_index < len(self.open_tabs): + current_active = self.open_tabs[self.active_tab_index] + self.files[current_active] = editor.text + + if self.open_tabs and self.active_tab_index < len(self.open_tabs): + target_file = self.open_tabs[self.active_tab_index] + target_code = self.files.get(target_file, "") + editor.text = target_code + lang_key = get_tui_language(target_file) + try: + if lang_key in editor.available_languages: + editor.language = lang_key + else: + editor.language = None + except Exception: + editor.language = None + self.update_tab_bar_display() + + def action_select_tab_index(self, idx: int) -> None: + """Switch active tab by index (0-based).""" + if 0 <= idx < len(self.open_tabs): + self.sync_editor_to_active_tab(save_current=True) + self.active_tab_index = idx + self.sync_editor_to_active_tab(save_current=False) + self.query_one("#status-bar", Static).update(f"Focused tab: {self.open_tabs[idx]}") + + def action_select_tab_1(self) -> None: self.action_select_tab_index(0) + def action_select_tab_2(self) -> None: self.action_select_tab_index(1) + def action_select_tab_3(self) -> None: self.action_select_tab_index(2) + def action_select_tab_4(self) -> None: self.action_select_tab_index(3) + def action_select_tab_5(self) -> None: self.action_select_tab_index(4) + def action_select_tab_6(self) -> None: self.action_select_tab_index(5) + def action_select_tab_7(self) -> None: self.action_select_tab_index(6) + def action_select_tab_8(self) -> None: self.action_select_tab_index(7) + def action_select_tab_9(self) -> None: self.action_select_tab_index(8) + + def action_prev_tab(self) -> None: + if self.open_tabs: + new_idx = (self.active_tab_index - 1) % len(self.open_tabs) + self.action_select_tab_index(new_idx) + + def action_next_tab(self) -> None: + if self.open_tabs: + new_idx = (self.active_tab_index + 1) % len(self.open_tabs) + self.action_select_tab_index(new_idx) + + def action_close_tab(self) -> None: + if len(self.open_tabs) <= 1: + self.query_one("#status-bar", Static).update("Cannot close the only open tab.") + return + closed_file = self.open_tabs[self.active_tab_index] + self.open_tabs.pop(self.active_tab_index) + self.active_tab_index = min(self.active_tab_index, len(self.open_tabs) - 1) + self.sync_editor_to_active_tab(save_current=False) + self.query_one("#status-bar", Static).update(f"Closed tab '{closed_file}'") + + def action_prompt_new_tab(self) -> None: + panel = self.query_one("#editor-panel") + panel.add_class("prompt-active") + self.file_modal_mode = "new_file" + inp = self.query_one("#file-prompt-input", Input) + inp.placeholder = "Enter new filename (e.g. helper.cpp, utils.h)..." + inp.value = "" + inp.focus() + self.query_one("#status-bar", Static).update("Creating new file tab... Type name and press Enter.") + + def action_prompt_rename_tab(self) -> None: + if not self.open_tabs: + return + panel = self.query_one("#editor-panel") + panel.add_class("prompt-active") + self.file_modal_mode = "rename_file" + current_name = self.open_tabs[self.active_tab_index] + inp = self.query_one("#file-prompt-input", Input) + inp.placeholder = f"Enter new name for '{current_name}'..." + inp.value = current_name + inp.focus() + self.query_one("#status-bar", Static).update(f"Renaming active tab '{current_name}'... Type new name and press Enter.") + + def action_prompt_new_folder(self) -> None: + panel = self.query_one("#editor-panel") + panel.add_class("prompt-active") + self.file_modal_mode = "new_folder" + inp = self.query_one("#file-prompt-input", Input) + inp.placeholder = "Enter new folder name (e.g. src, include)..." + inp.value = "" + inp.focus() + self.query_one("#status-bar", Static).update("Creating new folder... Type folder name and press Enter.") + + def submit_file_prompt(self) -> None: + panel = self.query_one("#editor-panel") + panel.remove_class("prompt-active") + inp = self.query_one("#file-prompt-input", Input) + val = inp.value.strip() + inp.value = "" + + if not val: + self.query_one("#editor", CodeEditor).focus() + return + + if self.file_modal_mode in ("new_file", "new_tab"): + if val not in self.files: + self.files[val] = "" + if val not in self.open_tabs: + self.open_tabs.append(val) + self.sync_editor_to_active_tab(save_current=True) + self.active_tab_index = self.open_tabs.index(val) + self.sync_editor_to_active_tab(save_current=False) + self.refresh_folder_tree() + self.query_one("#status-bar", Static).update(f"Created & opened '{val}'") + self.query_one("#editor", CodeEditor).focus() + elif self.file_modal_mode == "rename_file": + old_name = self.open_tabs[self.active_tab_index] + content = self.files.get(old_name, "") + if old_name in self.files: + del self.files[old_name] + self.files[val] = content + self.open_tabs[self.active_tab_index] = val + self.sync_editor_to_active_tab(save_current=False) + self.refresh_folder_tree() + self.query_one("#status-bar", Static).update(f"Renamed '{old_name}' -> '{val}'") + self.query_one("#editor", CodeEditor).focus() + elif self.file_modal_mode == "new_folder": + if val not in self.folders: + self.folders.append(val) + self.refresh_folder_tree() + main_box = self.query_one("#main-container") + if main_box.has_class("sidebar-folder-open"): + self.query_one("#folder_tree_list", OptionList).focus() + self.query_one("#status-bar", Static).update( + f"Created folder '{val}' | 📁 File Browser | Navigate: ↑/↓ & Enter | Ctrl+N: New Folder | Ctrl+Arrows: Move" + ) + return + self.query_one("#status-bar", Static).update(f"Created folder '{val}'") + self.query_one("#editor", CodeEditor).focus() + + def action_focus_challenges(self) -> None: + """Focus challenge selection list (Ctrl+J).""" + self.query_one("#challenge_list", OptionList).focus() + + def action_focus_editor(self) -> None: + """Focus main code editor text input (Ctrl+K).""" + self.query_one("#editor", CodeEditor).focus() + + def action_focus_sidebar(self) -> None: + """Focus currently open sidebar (Ctrl+L). Does not display focus text when all sidebars are closed.""" + main_box = self.query_one("#main-container") + if main_box.has_class("sidebar-folder-open"): + self.query_one("#folder_tree_list", OptionList).focus() + elif main_box.has_class("sidebar-mentor-open"): + self.query_one("#mentor-question-input", MentorInputTextArea).focus() + elif main_box.has_class("sidebar-handbook-open"): + self.query_one("#handbook_topic_list", OptionList).focus() + + def action_toggle_folder_sidebar(self) -> None: + """Toggle Folder Tree Sidebar on right hand side (Ctrl+F) and toggle off mentor/handbook sidebars.""" + main_box = self.query_one("#main-container") + if main_box.has_class("sidebar-folder-open"): + main_box.remove_class("sidebar-folder-open") + main_box.remove_class("sidebar-open") + self.query_one("#editor", CodeEditor).focus() + self.query_one("#status-bar", Static).update("Ready") + else: + main_box.remove_class("sidebar-mentor-open") + main_box.remove_class("sidebar-handbook-open") + main_box.add_class("sidebar-open") + main_box.add_class("sidebar-folder-open") + self.refresh_folder_tree() + folder_tree = self.query_one("#folder_tree_list", OptionList) + folder_tree.focus() + self.query_one("#status-bar", Static).update( + "📁 File Browser | Navigate: ↑/↓ & Enter | Ctrl+N: New Folder | Ctrl+T: New File | Ctrl+R: Rename | Ctrl+Arrows: Move" + ) + + def refresh_folder_tree(self) -> None: + """Refresh contents of OptionList inside #folder-sidebar.""" + try: + tree_list = self.query_one("#folder_tree_list", OptionList) + tree_list.clear_options() + self.folder_tree_items = [] + + for folder in sorted(self.folders): + tree_list.add_option(Option(f"📁 {folder}/", id=f"dir:{folder}")) + self.folder_tree_items.append({"type": "folder", "path": folder}) + + for filename in sorted(self.files.keys()): + active_marker = " ◀" if (self.open_tabs and filename == self.open_tabs[self.active_tab_index]) else "" + tree_list.add_option(Option(f"📄 {filename}{active_marker}", id=f"file:{filename}")) + self.folder_tree_items.append({"type": "file", "path": filename}) + except Exception: + pass + + def action_scroll_mentor_up_or_move(self) -> None: + """Handles Ctrl+Up: move item up if folder tree is focused, else scroll mentor.""" + focused = self.focused + if focused and getattr(focused, "id", None) == "folder_tree_list": + tree_list = self.query_one("#folder_tree_list", OptionList) + curr = tree_list.highlighted + if curr is not None and curr > 0: + self.folder_tree_items[curr], self.folder_tree_items[curr - 1] = ( + self.folder_tree_items[curr - 1], self.folder_tree_items[curr] + ) + tree_list.highlighted = curr - 1 + self.query_one("#status-bar", Static).update("Moved item up in folder tree.") + else: + self.action_scroll_mentor_up() + + def action_scroll_mentor_down_or_move(self) -> None: + """Handles Ctrl+Down: move item down if folder tree is focused, else scroll mentor.""" + focused = self.focused + if focused and getattr(focused, "id", None) == "folder_tree_list": + tree_list = self.query_one("#folder_tree_list", OptionList) + curr = tree_list.highlighted + if curr is not None and curr < len(self.folder_tree_items) - 1: + self.folder_tree_items[curr], self.folder_tree_items[curr + 1] = ( + self.folder_tree_items[curr + 1], self.folder_tree_items[curr] + ) + tree_list.highlighted = curr + 1 + self.query_one("#status-bar", Static).update("Moved item down in folder tree.") + else: + self.action_scroll_mentor_down() + + def action_move_folder_left(self) -> None: + focused = self.focused + if focused and getattr(focused, "id", None) == "folder_tree_list": + self.query_one("#status-bar", Static).update("Moved item out of directory.") + + def action_move_folder_right(self) -> None: + focused = self.focused + if focused and getattr(focused, "id", None) == "folder_tree_list": + self.query_one("#status-bar", Static).update("Moved item into directory.") def toggle_expand_output(self) -> None: """Toggle output terminal expansion and stdin input bar.""" @@ -518,6 +1177,7 @@ class TactiTermTUI(App): self.query_one("#editor", CodeEditor).focus() self.query_one("#status-bar", Static).update("Mentor sidebar closed.") else: + main_box.remove_class("sidebar-folder-open") main_box.remove_class("sidebar-handbook-open") main_box.add_class("sidebar-open") main_box.add_class("sidebar-mentor-open") @@ -533,6 +1193,7 @@ class TactiTermTUI(App): self.query_one("#editor", CodeEditor).focus() self.query_one("#status-bar", Static).update("Handbook sidebar closed.") else: + main_box.remove_class("sidebar-folder-open") main_box.remove_class("sidebar-mentor-open") main_box.add_class("sidebar-open") main_box.add_class("sidebar-handbook-open") @@ -560,7 +1221,10 @@ class TactiTermTUI(App): q_input = self.query_one("#mentor-question-input", MentorInputTextArea) user_question = q_input.text.strip() if question is None else question.strip() - code = self.query_one("#editor", CodeEditor).text + + self.sync_editor_to_active_tab(save_current=True) + active_file = self.open_tabs[self.active_tab_index] if self.open_tabs else "main.py" + code = self.files.get(active_file, "") main_box = self.query_one("#main-container") main_box.remove_class("sidebar-handbook-open") @@ -573,7 +1237,7 @@ class TactiTermTUI(App): self.query_one("#status-bar", Static).update( f"⏳ Consulting Mentor at {mentor.config.llm_base_url}..." ) - self.run_worker(self._mentor_worker(challenge, code, user_question)) + self.run_worker(self._mentor_worker(challenge, code, user_question, files=self.files, active_file=active_file)) def action_check_answer(self) -> None: if not self.active_challenge_id: @@ -584,7 +1248,10 @@ class TactiTermTUI(App): if not challenge: return - code = self.query_one("#editor", CodeEditor).text + self.sync_editor_to_active_tab(save_current=True) + active_file = self.open_tabs[self.active_tab_index] if self.open_tabs else "main.py" + code = self.files.get(active_file, "") + if not code.strip(): self.query_one("#status-bar", Static).update("Editor is empty — write code before checking answer.") return @@ -603,17 +1270,24 @@ class TactiTermTUI(App): self.query_one("#mentor-response-display", Markdown).display = False self.query_one("#mentor-loading-indicator", LoadingIndicator).display = True self.query_one("#status-bar", Static).update("⏳ Consulting Mentor to check answer...") - self.run_worker(self._mentor_worker(challenge, code, check_question)) + self.run_worker(self._mentor_worker(challenge, code, check_question, files=self.files, active_file=active_file)) def action_unfocus_editor(self) -> None: main_box = self.query_one("#main-container") + if main_box.has_class("show-theme-menu"): + main_box.remove_class("show-theme-menu") + self.query_one("#editor", CodeEditor).focus() + return + if main_box.has_class("show-help"): + main_box.remove_class("show-help") + self.query_one("#editor", CodeEditor).focus() + return if main_box.has_class("show-completion"): main_box.remove_class("show-completion") self.query_one("#editor", CodeEditor).focus() return self.query_one("#challenge_list", OptionList).focus() - self.query_one("#status-bar", Static).update("Focus moved to Challenge List.") def action_toggle_panel_focus(self) -> None: focused = self.focused @@ -656,15 +1330,12 @@ class TactiTermTUI(App): """ self.query_one("#challenge-details", Markdown).update(markdown_content) - editor = self.query_one("#editor", CodeEditor) - lang_key = get_tui_language(challenge.language) - try: - if lang_key in editor.available_languages: - editor.language = lang_key - else: - editor.language = None - except Exception: - editor.language = None + # Initialize multi-file project defaults for challenge language + self.files = get_tui_default_files(challenge.language, enable_boilerplate=config.enable_boilerplate) + self.open_tabs = list(self.files.keys()) + self.active_tab_index = 0 + self.sync_editor_to_active_tab(save_current=False) + self.refresh_folder_tree() main_box = self.query_one("#main-container") if main_box.has_class("sidebar-handbook-open"): @@ -680,16 +1351,46 @@ class TactiTermTUI(App): self.query_one("#status-bar", Static).update( f"Hovering Challenge: {selected_option.prompt} — Hit Enter to jump to Editor" ) + elif event.option_list.id == "theme_list_popup": + selected_option = event.option + if selected_option and selected_option.id and not str(selected_option.id).startswith("action:"): + self.apply_theme(selected_option.id) def on_option_list_option_selected( self, event: OptionList.OptionSelected ) -> None: - if event.option_list.id == "challenge_list": + if event.option_list.id == "theme_list_popup": + selected_option = event.option + if selected_option and selected_option.id: + if str(selected_option.id) == "action:refresh": + theme_manager.load_all_themes() + self.refresh_theme_list_popup() + count = len(theme_manager.get_theme_ids()) + self.query_one("#status-bar", Static).update(f"🔄 Re-scanned theme folders! {count} themes loaded.") + return + self.apply_theme(selected_option.id) + main_box = self.query_one("#main-container") + main_box.remove_class("show-theme-menu") + self.query_one("#editor", CodeEditor).focus() + elif event.option_list.id == "challenge_list": selected_option = event.option if selected_option and selected_option.id: self._display_challenge(selected_option.id) self.query_one("#editor", CodeEditor).focus() self.query_one("#status-bar", Static).update("Focused: Code Editor") + elif event.option_list.id == "folder_tree_list": + selected_option = event.option + if selected_option and selected_option.id: + opt_id = str(selected_option.id) + if opt_id.startswith("file:"): + target = opt_id.replace("file:", "") + if target not in self.open_tabs: + self.open_tabs.append(target) + self.sync_editor_to_active_tab(save_current=True) + self.active_tab_index = self.open_tabs.index(target) + self.sync_editor_to_active_tab(save_current=False) + self.query_one("#editor", CodeEditor).focus() + self.query_one("#status-bar", Static).update(f"Opened '{target}' in editor") elif event.option_list.id == "completion_list_popup": selected_option = event.option if selected_option and selected_option.prompt: @@ -798,26 +1499,17 @@ class TactiTermTUI(App): editor_left_rel = editor.region.x - panel.region.x editor_height = editor.region.height panel_width = panel.region.width + + top = editor_top_rel + vis_row + 1 + if top + popup_height > editor_top_rel + editor_height: + top = max(0, editor_top_rel + vis_row - popup_height) + + left = max(0, editor_left_rel + gutter + vis_col) + if left + 32 > panel_width: + left = max(0, panel_width - 32) except Exception: - editor_top_rel = 2 - editor_left_rel = 0 - editor_height = 15 - panel_width = 80 - - # Line Y position relative to panel (1 cell for top border of editor) - line_y = editor_top_rel + 1 + vis_row - - # Smart placement: position BELOW cursor line if space permits, else ABOVE cursor line - if line_y + 1 + popup_height <= editor_top_rel + editor_height: - top = line_y + 1 - else: - top = max(editor_top_rel + 1, line_y - popup_height) - - # X position: editor left rel + 1 for border + gutter + vis_col - left_pos = editor_left_rel + 1 + gutter + vis_col - popup_width = 32 - max_left = max(0, panel_width - popup_width) - left = min(max(0, left_pos), max_left) + top = max(1, vis_row + 1) + left = max(1, gutter + vis_col) popup.styles.offset = (int(left), int(top)) @@ -895,9 +1587,18 @@ class TactiTermTUI(App): self.query_one("#status-bar", Static).update("Refreshing challenges...") self.load_challenges() - async def _mentor_worker(self, challenge: object, code: str, user_question: str) -> None: + async def _mentor_worker( + self, + challenge: object, + code: str, + user_question: str, + files: Dict[str, str] = None, + active_file: str = None, + ) -> None: try: - res = await mentor.get_guidance_async(challenge, code, user_question=user_question) + res = await mentor.get_guidance_async( + challenge, code, user_question=user_question, files=files, active_file=active_file + ) response_text = res.get("mentor_response", "No response received.") q_header = f"### Question / Evaluation:\n> {user_question}\n\n---\n\n" if user_question else "" @@ -922,7 +1623,10 @@ class TactiTermTUI(App): self.query_one("#status-bar", Static).update("✗ Failed to generate reference card") def action_lint_code(self) -> None: - code = self.query_one("#editor", CodeEditor).text + self.sync_editor_to_active_tab(save_current=True) + active_file = self.open_tabs[self.active_tab_index] if self.open_tabs else "main.py" + code = self.files.get(active_file, "") + if not code.strip(): self.query_one("#status-bar", Static).update("Editor is empty — nothing to lint.") return @@ -931,7 +1635,7 @@ class TactiTermTUI(App): challenge = self.challenges_dict.get(self.active_challenge_id) lang = challenge.language.lower() if challenge else "python" - res = linter.lint(lang, code) + res = linter.lint(lang, code=code, files=self.files, active_file=active_file) output_widget = self.query_one("#output-container", Static) if res.get("exit_code") == 0: @@ -955,7 +1659,10 @@ class TactiTermTUI(App): self.query_one("#status-bar", Static).update("Lint complete") def action_run_code(self, stdin: str = "") -> None: - code = self.query_one("#editor", CodeEditor).text + self.sync_editor_to_active_tab(save_current=True) + active_file = self.open_tabs[self.active_tab_index] if self.open_tabs else "main.py" + code = self.files.get(active_file, "") + if not code.strip(): self.query_one("#status-bar", Static).update("Editor is empty — write code before running.") return @@ -964,7 +1671,7 @@ class TactiTermTUI(App): challenge = self.challenges_dict.get(self.active_challenge_id) lang = challenge.language.lower() if challenge else "python" - res = executor.run(lang, code, stdin=stdin) + res = executor.run(lang, code=code, stdin=stdin, files=self.files) output_widget = self.query_one("#output-container", Static) if res.get("exit_code") == 0: diff --git a/src/tui/theme_manager.py b/src/tui/theme_manager.py new file mode 100644 index 0000000..31c4992 --- /dev/null +++ b/src/tui/theme_manager.py @@ -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() diff --git a/src/tui/themes/adw.json b/src/tui/themes/adw.json new file mode 100644 index 0000000..c3b12ce --- /dev/null +++ b/src/tui/themes/adw.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/atuel.json b/src/tui/themes/atuel.json new file mode 100644 index 0000000..408f0a8 --- /dev/null +++ b/src/tui/themes/atuel.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/ayu-blue.json b/src/tui/themes/ayu-blue.json new file mode 100644 index 0000000..b69e463 --- /dev/null +++ b/src/tui/themes/ayu-blue.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/ayu-green.json b/src/tui/themes/ayu-green.json new file mode 100644 index 0000000..4407e5b --- /dev/null +++ b/src/tui/themes/ayu-green.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/ayu-red.json b/src/tui/themes/ayu-red.json new file mode 100644 index 0000000..b61a1c3 --- /dev/null +++ b/src/tui/themes/ayu-red.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/breeze.json b/src/tui/themes/breeze.json new file mode 100644 index 0000000..15bdf51 --- /dev/null +++ b/src/tui/themes/breeze.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-frappe-blue.json b/src/tui/themes/catppuccin-frappe-blue.json new file mode 100644 index 0000000..6bc3dc6 --- /dev/null +++ b/src/tui/themes/catppuccin-frappe-blue.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-frappe-lavender.json b/src/tui/themes/catppuccin-frappe-lavender.json new file mode 100644 index 0000000..83e0ba8 --- /dev/null +++ b/src/tui/themes/catppuccin-frappe-lavender.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-frappe-mauve.json b/src/tui/themes/catppuccin-frappe-mauve.json new file mode 100644 index 0000000..a941ba4 --- /dev/null +++ b/src/tui/themes/catppuccin-frappe-mauve.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-frappe-pink.json b/src/tui/themes/catppuccin-frappe-pink.json new file mode 100644 index 0000000..36578d9 --- /dev/null +++ b/src/tui/themes/catppuccin-frappe-pink.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-frappe-rosewater.json b/src/tui/themes/catppuccin-frappe-rosewater.json new file mode 100644 index 0000000..62f3129 --- /dev/null +++ b/src/tui/themes/catppuccin-frappe-rosewater.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-frappe-sapphirejson.json b/src/tui/themes/catppuccin-frappe-sapphirejson.json new file mode 100644 index 0000000..3ea5625 --- /dev/null +++ b/src/tui/themes/catppuccin-frappe-sapphirejson.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-lavender.json b/src/tui/themes/catppuccin-lavender.json new file mode 100644 index 0000000..c03b995 --- /dev/null +++ b/src/tui/themes/catppuccin-lavender.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-macchiato-lavender.json b/src/tui/themes/catppuccin-macchiato-lavender.json new file mode 100644 index 0000000..8b1fb39 --- /dev/null +++ b/src/tui/themes/catppuccin-macchiato-lavender.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-macchiato-mauve.json b/src/tui/themes/catppuccin-macchiato-mauve.json new file mode 100644 index 0000000..88b73f5 --- /dev/null +++ b/src/tui/themes/catppuccin-macchiato-mauve.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-macchiato-pink.json b/src/tui/themes/catppuccin-macchiato-pink.json new file mode 100644 index 0000000..dcb5273 --- /dev/null +++ b/src/tui/themes/catppuccin-macchiato-pink.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-macchiato-rosewater.json b/src/tui/themes/catppuccin-macchiato-rosewater.json new file mode 100644 index 0000000..8934af1 --- /dev/null +++ b/src/tui/themes/catppuccin-macchiato-rosewater.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-macchiato-sapphire.json b/src/tui/themes/catppuccin-macchiato-sapphire.json new file mode 100644 index 0000000..ab4dd23 --- /dev/null +++ b/src/tui/themes/catppuccin-macchiato-sapphire.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-mocha-pink.json b/src/tui/themes/catppuccin-mocha-pink.json new file mode 100644 index 0000000..a91e32e --- /dev/null +++ b/src/tui/themes/catppuccin-mocha-pink.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-mocha-rosewater.json b/src/tui/themes/catppuccin-mocha-rosewater.json new file mode 100644 index 0000000..556cfc4 --- /dev/null +++ b/src/tui/themes/catppuccin-mocha-rosewater.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin-mocha-sapphire.json b/src/tui/themes/catppuccin-mocha-sapphire.json new file mode 100644 index 0000000..5c99ccb --- /dev/null +++ b/src/tui/themes/catppuccin-mocha-sapphire.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/catppuccin.json b/src/tui/themes/catppuccin.json new file mode 100644 index 0000000..84cd40d --- /dev/null +++ b/src/tui/themes/catppuccin.json @@ -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" +} diff --git a/src/tui/themes/cherry-blossom.json b/src/tui/themes/cherry-blossom.json new file mode 100644 index 0000000..8a6157f --- /dev/null +++ b/src/tui/themes/cherry-blossom.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/cream-autumn.json b/src/tui/themes/cream-autumn.json new file mode 100644 index 0000000..53d05ae --- /dev/null +++ b/src/tui/themes/cream-autumn.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/cream.json b/src/tui/themes/cream.json new file mode 100644 index 0000000..75a0e20 --- /dev/null +++ b/src/tui/themes/cream.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/creamy-forest.json b/src/tui/themes/creamy-forest.json new file mode 100644 index 0000000..a36cce5 --- /dev/null +++ b/src/tui/themes/creamy-forest.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/cyberpunk.json b/src/tui/themes/cyberpunk.json new file mode 100644 index 0000000..e147d67 --- /dev/null +++ b/src/tui/themes/cyberpunk.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/default.json b/src/tui/themes/default.json new file mode 100644 index 0000000..ab4aba6 --- /dev/null +++ b/src/tui/themes/default.json @@ -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" +} diff --git a/src/tui/themes/doomed.json b/src/tui/themes/doomed.json new file mode 100644 index 0000000..14e43cf --- /dev/null +++ b/src/tui/themes/doomed.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/dracula.json b/src/tui/themes/dracula.json new file mode 100644 index 0000000..7826373 --- /dev/null +++ b/src/tui/themes/dracula.json @@ -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" +} diff --git a/src/tui/themes/espresso-cream.json b/src/tui/themes/espresso-cream.json new file mode 100644 index 0000000..81176a0 --- /dev/null +++ b/src/tui/themes/espresso-cream.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/everdeer.json b/src/tui/themes/everdeer.json new file mode 100644 index 0000000..460fa5f --- /dev/null +++ b/src/tui/themes/everdeer.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/everforest-alt.json b/src/tui/themes/everforest-alt.json new file mode 100644 index 0000000..f091685 --- /dev/null +++ b/src/tui/themes/everforest-alt.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/everforest-material.json b/src/tui/themes/everforest-material.json new file mode 100644 index 0000000..69ce257 --- /dev/null +++ b/src/tui/themes/everforest-material.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/everforest.json b/src/tui/themes/everforest.json new file mode 100644 index 0000000..3620969 --- /dev/null +++ b/src/tui/themes/everforest.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/flexoki.json b/src/tui/themes/flexoki.json new file mode 100644 index 0000000..d3d034e --- /dev/null +++ b/src/tui/themes/flexoki.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/garnet.json b/src/tui/themes/garnet.json new file mode 100644 index 0000000..e1f2ca9 --- /dev/null +++ b/src/tui/themes/garnet.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/github-dark.json b/src/tui/themes/github-dark.json new file mode 100644 index 0000000..e954f5e --- /dev/null +++ b/src/tui/themes/github-dark.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/gruber-darker.json b/src/tui/themes/gruber-darker.json new file mode 100644 index 0000000..e768302 --- /dev/null +++ b/src/tui/themes/gruber-darker.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/gruvbox-material.json b/src/tui/themes/gruvbox-material.json new file mode 100644 index 0000000..748d35e --- /dev/null +++ b/src/tui/themes/gruvbox-material.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/gruvbox.json b/src/tui/themes/gruvbox.json new file mode 100644 index 0000000..be0d86b --- /dev/null +++ b/src/tui/themes/gruvbox.json @@ -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" +} diff --git a/src/tui/themes/gruvboxalt.json b/src/tui/themes/gruvboxalt.json new file mode 100644 index 0000000..7a5c3a1 --- /dev/null +++ b/src/tui/themes/gruvboxalt.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/hexa34c.json b/src/tui/themes/hexa34c.json new file mode 100644 index 0000000..b2aaab8 --- /dev/null +++ b/src/tui/themes/hexa34c.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/horizon.json b/src/tui/themes/horizon.json new file mode 100644 index 0000000..008f308 --- /dev/null +++ b/src/tui/themes/horizon.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/jiva-lotus.json b/src/tui/themes/jiva-lotus.json new file mode 100644 index 0000000..d48b0c6 --- /dev/null +++ b/src/tui/themes/jiva-lotus.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/jiva-paper.json b/src/tui/themes/jiva-paper.json new file mode 100644 index 0000000..8fffc98 --- /dev/null +++ b/src/tui/themes/jiva-paper.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/jiva.json b/src/tui/themes/jiva.json new file mode 100644 index 0000000..130d97f --- /dev/null +++ b/src/tui/themes/jiva.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/kanagawa-dragon.json b/src/tui/themes/kanagawa-dragon.json new file mode 100644 index 0000000..93cbf83 --- /dev/null +++ b/src/tui/themes/kanagawa-dragon.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/kanagawa-kasumi.json b/src/tui/themes/kanagawa-kasumi.json new file mode 100644 index 0000000..5abcdb0 --- /dev/null +++ b/src/tui/themes/kanagawa-kasumi.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/kanagawa-paper.json b/src/tui/themes/kanagawa-paper.json new file mode 100644 index 0000000..ce30ab6 --- /dev/null +++ b/src/tui/themes/kanagawa-paper.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/kemuri-koke.json b/src/tui/themes/kemuri-koke.json new file mode 100644 index 0000000..c635ab3 --- /dev/null +++ b/src/tui/themes/kemuri-koke.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/kemuri-susu.json b/src/tui/themes/kemuri-susu.json new file mode 100644 index 0000000..232d6f1 --- /dev/null +++ b/src/tui/themes/kemuri-susu.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/kemuri.json b/src/tui/themes/kemuri.json new file mode 100644 index 0000000..e9e06b9 --- /dev/null +++ b/src/tui/themes/kemuri.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/lilac-amoled.json b/src/tui/themes/lilac-amoled.json new file mode 100644 index 0000000..939704f --- /dev/null +++ b/src/tui/themes/lilac-amoled.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/macaron.json b/src/tui/themes/macaron.json new file mode 100644 index 0000000..0074970 --- /dev/null +++ b/src/tui/themes/macaron.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/matecito.json b/src/tui/themes/matecito.json new file mode 100644 index 0000000..b4f44e6 --- /dev/null +++ b/src/tui/themes/matecito.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/miasma.json b/src/tui/themes/miasma.json new file mode 100644 index 0000000..254cc72 --- /dev/null +++ b/src/tui/themes/miasma.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/mine.json b/src/tui/themes/mine.json new file mode 100644 index 0000000..62cf6de --- /dev/null +++ b/src/tui/themes/mine.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/mizuki-akiyama.json b/src/tui/themes/mizuki-akiyama.json new file mode 100644 index 0000000..8b855fa --- /dev/null +++ b/src/tui/themes/mizuki-akiyama.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/monochrome.json b/src/tui/themes/monochrome.json new file mode 100644 index 0000000..838e7d1 --- /dev/null +++ b/src/tui/themes/monochrome.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/murasaki.json b/src/tui/themes/murasaki.json new file mode 100644 index 0000000..ea6f548 --- /dev/null +++ b/src/tui/themes/murasaki.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/murata.json b/src/tui/themes/murata.json new file mode 100644 index 0000000..273d2a3 --- /dev/null +++ b/src/tui/themes/murata.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/naysayer.json b/src/tui/themes/naysayer.json new file mode 100644 index 0000000..15c7e8c --- /dev/null +++ b/src/tui/themes/naysayer.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/neon-surf.json b/src/tui/themes/neon-surf.json new file mode 100644 index 0000000..7957606 --- /dev/null +++ b/src/tui/themes/neon-surf.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/noctalia-legacy.json b/src/tui/themes/noctalia-legacy.json new file mode 100644 index 0000000..171fd9f --- /dev/null +++ b/src/tui/themes/noctalia-legacy.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/nord-aurora.json b/src/tui/themes/nord-aurora.json new file mode 100644 index 0000000..9203427 --- /dev/null +++ b/src/tui/themes/nord-aurora.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/nord.json b/src/tui/themes/nord.json new file mode 100644 index 0000000..dfd6044 --- /dev/null +++ b/src/tui/themes/nord.json @@ -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" +} diff --git a/src/tui/themes/oasis-abyss.json b/src/tui/themes/oasis-abyss.json new file mode 100644 index 0000000..6cd0f33 --- /dev/null +++ b/src/tui/themes/oasis-abyss.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/occult-umbral.json b/src/tui/themes/occult-umbral.json new file mode 100644 index 0000000..c38ac17 --- /dev/null +++ b/src/tui/themes/occult-umbral.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/one-dark-two.json b/src/tui/themes/one-dark-two.json new file mode 100644 index 0000000..53b4fc7 --- /dev/null +++ b/src/tui/themes/one-dark-two.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/one.json b/src/tui/themes/one.json new file mode 100644 index 0000000..d1353de --- /dev/null +++ b/src/tui/themes/one.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/osaka-jade.json b/src/tui/themes/osaka-jade.json new file mode 100644 index 0000000..4af515c --- /dev/null +++ b/src/tui/themes/osaka-jade.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/oxide.json b/src/tui/themes/oxide.json new file mode 100644 index 0000000..ed6cf8b --- /dev/null +++ b/src/tui/themes/oxide.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/oxocarbon.json b/src/tui/themes/oxocarbon.json new file mode 100644 index 0000000..39ba528 --- /dev/null +++ b/src/tui/themes/oxocarbon.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/paradise.json b/src/tui/themes/paradise.json new file mode 100644 index 0000000..38d4328 --- /dev/null +++ b/src/tui/themes/paradise.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/peche.json b/src/tui/themes/peche.json new file mode 100644 index 0000000..ac66606 --- /dev/null +++ b/src/tui/themes/peche.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/rose-pine-alt.json b/src/tui/themes/rose-pine-alt.json new file mode 100644 index 0000000..7ecf044 --- /dev/null +++ b/src/tui/themes/rose-pine-alt.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/rose-pine-moon-alt.json b/src/tui/themes/rose-pine-moon-alt.json new file mode 100644 index 0000000..7c877b6 --- /dev/null +++ b/src/tui/themes/rose-pine-moon-alt.json @@ -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" +} \ No newline at end of file diff --git a/src/tui/themes/rose-pine-moon.json b/src/tui/themes/rose-pine-moon.json new file mode 100644 index 0000000..11c9ad0 --- /dev/null +++ b/src/tui/themes/rose-pine-moon.json @@ -0,0 +1,12 @@ +{ + "id": "rose-pine-moon", + "name": "Rose Pine Moon", + "background": "#393552", + "surface": "#232136", + "primary": "#ea9a97", + "primary_text": "#232136", + "secondary": "#9ccfd8", + "accent": "#3e8fb0", + "folder_header": "#3e8fb0", + "folder_text": "#e0def4" +} \ No newline at end of file diff --git a/src/tui/themes/rosey-amoled.json b/src/tui/themes/rosey-amoled.json new file mode 100644 index 0000000..be90355 --- /dev/null +++ b/src/tui/themes/rosey-amoled.json @@ -0,0 +1,12 @@ +{ + "id": "rosey-amoled", + "name": "Rosey AMOLED", + "background": "#1a0f16", + "surface": "#000000", + "primary": "#ff8cb3", + "primary_text": "#000000", + "secondary": "#ffb3cc", + "accent": "#ffcce0", + "folder_header": "#ffcce0", + "folder_text": "#000000" +} \ No newline at end of file diff --git a/src/tui/themes/shien.json b/src/tui/themes/shien.json new file mode 100644 index 0000000..35e3c25 --- /dev/null +++ b/src/tui/themes/shien.json @@ -0,0 +1,12 @@ +{ + "id": "shien", + "name": "Shien", + "background": "#24202C", + "surface": "#15131B", + "primary": "#9B8BC1", + "primary_text": "#15131B", + "secondary": "#7866A3", + "accent": "#5D507C", + "folder_header": "#5D507C", + "folder_text": "#FFFFFF" +} \ No newline at end of file diff --git a/src/tui/themes/shinonome.json b/src/tui/themes/shinonome.json new file mode 100644 index 0000000..e2d3022 --- /dev/null +++ b/src/tui/themes/shinonome.json @@ -0,0 +1,12 @@ +{ + "id": "shinonome", + "name": "Shinonome", + "background": "#2D3339", + "surface": "#1A1D20", + "primary": "#D5ACA9", + "primary_text": "#1A1D20", + "secondary": "#B38D97", + "accent": "#C5BAAF", + "folder_header": "#C5BAAF", + "folder_text": "#1A1D20" +} \ No newline at end of file diff --git a/src/tui/themes/solarized-osaka.json b/src/tui/themes/solarized-osaka.json new file mode 100644 index 0000000..133db9c --- /dev/null +++ b/src/tui/themes/solarized-osaka.json @@ -0,0 +1,12 @@ +{ + "id": "solarized-osaka", + "name": "Solarized Osaka", + "background": "#002c38", + "surface": "#001419", + "primary": "#29a298", + "primary_text": "#ffffff", + "secondary": "#db302d", + "accent": "#b28500", + "folder_header": "#b28500", + "folder_text": "#001419" +} \ No newline at end of file diff --git a/src/tui/themes/solarized.json b/src/tui/themes/solarized.json new file mode 100644 index 0000000..fe23202 --- /dev/null +++ b/src/tui/themes/solarized.json @@ -0,0 +1,12 @@ +{ + "id": "solarized", + "name": "Solarized", + "background": "#073642", + "surface": "#002b36", + "primary": "#b58900", + "primary_text": "#002b36", + "secondary": "#d33682", + "accent": "#cb4b16", + "folder_header": "#cb4b16", + "folder_text": "#002b36" +} \ No newline at end of file diff --git a/src/tui/themes/sway-classic.json b/src/tui/themes/sway-classic.json new file mode 100644 index 0000000..56b2dc7 --- /dev/null +++ b/src/tui/themes/sway-classic.json @@ -0,0 +1,12 @@ +{ + "id": "sway-classic", + "name": "Sway Classic", + "background": "#333333", + "surface": "#222222", + "primary": "#4C7899", + "primary_text": "#FFFFFF", + "secondary": "#2E9EF4", + "accent": "#5F676A", + "folder_header": "#5F676A", + "folder_text": "#FFFFFF" +} \ No newline at end of file diff --git a/src/tui/themes/tokyo-night-moon.json b/src/tui/themes/tokyo-night-moon.json new file mode 100644 index 0000000..ed54ca3 --- /dev/null +++ b/src/tui/themes/tokyo-night-moon.json @@ -0,0 +1,12 @@ +{ + "id": "tokyo-night-moon", + "name": "Tokyo Night Moon", + "background": "#2c314a", + "surface": "#1f2335", + "primary": "#7a88cf", + "primary_text": "#1f2335", + "secondary": "#d7729f", + "accent": "#9cd58a", + "folder_header": "#9cd58a", + "folder_text": "#1f2335" +} \ No newline at end of file diff --git a/src/tui/themes/tokyo-night-storm.json b/src/tui/themes/tokyo-night-storm.json new file mode 100644 index 0000000..c675457 --- /dev/null +++ b/src/tui/themes/tokyo-night-storm.json @@ -0,0 +1,12 @@ +{ + "id": "tokyo-night-storm", + "name": "Tokyo Night Storm", + "background": "#1f2335", + "surface": "#24283b", + "primary": "#7aa2f7", + "primary_text": "#1a1b26", + "secondary": "#bb9af7", + "accent": "#73daca", + "folder_header": "#73daca", + "folder_text": "#1a1b26" +} \ No newline at end of file diff --git a/src/tui/themes/tokyo-night.json b/src/tui/themes/tokyo-night.json new file mode 100644 index 0000000..f1f541a --- /dev/null +++ b/src/tui/themes/tokyo-night.json @@ -0,0 +1,12 @@ +{ + "id": "tokyo-night", + "name": "Tokyo Night", + "background": "#1a1b26", + "surface": "#24283b", + "primary": "#7aa2f7", + "primary_text": "#1a1b26", + "secondary": "#bb9af7", + "accent": "#7dcfff", + "folder_header": "#bb9af7", + "folder_text": "#1a1b26" +} diff --git a/src/tui/themes/vesper.json b/src/tui/themes/vesper.json new file mode 100644 index 0000000..6a71e25 --- /dev/null +++ b/src/tui/themes/vesper.json @@ -0,0 +1,12 @@ +{ + "id": "vesper", + "name": "Vesper", + "background": "#1C1C1C", + "surface": "#0C0C0C", + "primary": "#FFC799", + "primary_text": "#000000", + "secondary": "#99FFE4", + "accent": "#FBADFF", + "folder_header": "#FBADFF", + "folder_text": "#000000" +} \ No newline at end of file diff --git a/src/tui/themes/zenbones.json b/src/tui/themes/zenbones.json new file mode 100644 index 0000000..5d78089 --- /dev/null +++ b/src/tui/themes/zenbones.json @@ -0,0 +1,12 @@ +{ + "id": "zenbones", + "name": "Zenbones", + "background": "#403833", + "surface": "#1c1917", + "primary": "#b4bdc3", + "primary_text": "#1c1917", + "secondary": "#888f94", + "accent": "#9f948f", + "folder_header": "#9f948f", + "folder_text": "#1c1917" +} \ No newline at end of file diff --git a/src/tui/widgets.py b/src/tui/widgets.py index 64dde51..679cd6c 100644 --- a/src/tui/widgets.py +++ b/src/tui/widgets.py @@ -383,6 +383,45 @@ class CodeEditor(TextArea): def _on_key(self, event: events.Key) -> None: """Handle completion key navigation, selection, escape, and Ctrl+E toggle.""" + special_keys = ( + "ctrl+r", "ctrl+t", "ctrl+a", "ctrl+d", "ctrl+f", "ctrl+h", "ctrl+p", + "ctrl+j", "ctrl+k", "ctrl+l", + "ctrl+alt+c", "ctrl+1", "ctrl+2", "ctrl+3", "ctrl+4", + "ctrl+5", "ctrl+6", "ctrl+7", "ctrl+8", "ctrl+9", + "ctrl+n", "ctrl+enter" + ) + if event.key in special_keys: + event.prevent_default() + event.stop() + if event.key == "ctrl+p": + self.app.action_cycle_theme() + elif event.key == "ctrl+h": + self.app.action_toggle_help_menu() + elif event.key == "ctrl+f": + self.app.action_toggle_folder_sidebar() + elif event.key == "ctrl+j": + self.app.action_focus_challenges() + elif event.key == "ctrl+k": + self.app.action_focus_editor() + elif event.key == "ctrl+l": + self.app.action_focus_sidebar() + elif event.key == "ctrl+r": + self.app.action_prompt_rename_tab() + elif event.key == "ctrl+t": + self.app.action_prompt_new_tab() + elif event.key == "ctrl+a": + self.app.action_prev_tab() + elif event.key == "ctrl+d": + self.app.action_next_tab() + elif event.key == "ctrl+n": + self.app.action_prompt_new_folder() + elif event.key == "ctrl+alt+c": + self.app.action_close_tab() + elif event.key.startswith("ctrl+") and event.key[5:].isdigit(): + digit = int(event.key[5:]) + self.app.action_select_tab_index(digit - 1) + return + if event.key == "ctrl+e": event.prevent_default() event.stop()