🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

practicode

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

practicode - npm Package Compare versions

Comparing version
0.1.5
to
0.1.6
+25
docs/ARCHITECTURE.md
# Architecture
Practicode is local-first: user data stays under `.practicode/`, `problems/`, and `submissions/`.
## Source Layout
- `src/core.rs` owns problem data, state loading/saving, judging, and file generation.
- `src/core/profile.rs` owns practice-profile defaults and normalization.
- `src/tui.rs` owns Ratatui rendering and interaction flow.
- `src/tui/commands.rs` owns the command palette catalog.
- `src/ai.rs` owns provider commands, daemon/model checks, and AI prompts.
- `src/update.rs` owns update checks.
- `src/text.rs` owns terminal text editing and markdown/plain rendering helpers.
## Extension Rules
- Add domain logic under the owning module first; keep `tui.rs` as orchestration and rendering.
- Add user-visible commands in `src/tui/commands.rs`, then route behavior in `PracticodeApp::handle_command`.
- Add persisted profile settings to `Settings`, normalize them in `normalize_settings`, and cover old-state compatibility with tests.
- Keep provider-specific behavior in `src/ai.rs`; TUI should ask for status or start tasks, not know provider internals.
- Keep local user data backwards-compatible. Missing fields should default cleanly.
## Release
See [MAINTAINING.md](MAINTAINING.md) for tag-based releases.
pub const DIFFICULTIES: &[&str] = &["auto", "easy", "medium", "hard"];
pub fn default_difficulty() -> String {
"auto".to_string()
}
pub fn normalize_difficulty(difficulty: &str) -> String {
let difficulty = difficulty.trim().to_lowercase();
if DIFFICULTIES.contains(&difficulty.as_str()) {
difficulty
} else {
default_difficulty()
}
}
pub fn parse_topic_list(value: &str) -> Vec<String> {
let mut topics = Vec::new();
for topic in value.split(',') {
let topic = topic.trim().trim_start_matches('#').trim().to_lowercase();
if !topic.is_empty() && !topics.contains(&topic) {
topics.push(topic);
}
}
topics
}
pub fn normalize_topic_list(topics: &[String]) -> Vec<String> {
parse_topic_list(&topics.join(","))
}
#[derive(Clone, Copy)]
pub(super) struct CommandHint {
pub(super) insert: &'static str,
pub(super) display: &'static str,
pub(super) desc_key: &'static str,
pub(super) keep_open: bool,
pub(super) help: bool,
}
pub(super) const COMMAND_HINTS: &[CommandHint] = &[
CommandHint {
insert: "run",
display: "/run",
desc_key: "cmd_run",
keep_open: false,
help: true,
},
CommandHint {
insert: "code",
display: "/code",
desc_key: "cmd_code",
keep_open: false,
help: true,
},
CommandHint {
insert: "next",
display: "/next",
desc_key: "cmd_next",
keep_open: false,
help: true,
},
CommandHint {
insert: "back",
display: "/back",
desc_key: "cmd_prev",
keep_open: false,
help: true,
},
CommandHint {
insert: "problems",
display: "/problems",
desc_key: "cmd_list",
keep_open: false,
help: true,
},
CommandHint {
insert: "open ",
display: "/open <id>",
desc_key: "cmd_open",
keep_open: true,
help: true,
},
CommandHint {
insert: "answer",
display: "/answer",
desc_key: "cmd_giveup",
keep_open: false,
help: true,
},
CommandHint {
insert: "hint ",
display: "/hint <request>",
desc_key: "cmd_hint",
keep_open: true,
help: true,
},
CommandHint {
insert: "profile",
display: "/profile",
desc_key: "cmd_profile",
keep_open: false,
help: true,
},
CommandHint {
insert: "difficulty auto",
display: "/difficulty auto",
desc_key: "cmd_difficulty",
keep_open: false,
help: true,
},
CommandHint {
insert: "difficulty easy",
display: "/difficulty easy",
desc_key: "cmd_difficulty",
keep_open: false,
help: false,
},
CommandHint {
insert: "difficulty medium",
display: "/difficulty medium",
desc_key: "cmd_difficulty",
keep_open: false,
help: false,
},
CommandHint {
insert: "difficulty hard",
display: "/difficulty hard",
desc_key: "cmd_difficulty",
keep_open: false,
help: false,
},
CommandHint {
insert: "topics ",
display: "/topics <list>",
desc_key: "cmd_topics",
keep_open: true,
help: true,
},
CommandHint {
insert: "avoid ",
display: "/avoid <list>",
desc_key: "cmd_avoid",
keep_open: true,
help: true,
},
CommandHint {
insert: "provider codex",
display: "/provider codex",
desc_key: "cmd_provider",
keep_open: false,
help: true,
},
CommandHint {
insert: "provider claude",
display: "/provider claude",
desc_key: "cmd_provider",
keep_open: false,
help: false,
},
CommandHint {
insert: "model auto",
display: "/model auto",
desc_key: "cmd_model_auto",
keep_open: false,
help: true,
},
CommandHint {
insert: "model ",
display: "/model <name>",
desc_key: "cmd_model_custom",
keep_open: true,
help: false,
},
CommandHint {
insert: "language python",
display: "/language python",
desc_key: "cmd_lang",
keep_open: false,
help: true,
},
CommandHint {
insert: "language ts",
display: "/language ts",
desc_key: "cmd_lang",
keep_open: false,
help: false,
},
CommandHint {
insert: "language java",
display: "/language java",
desc_key: "cmd_lang",
keep_open: false,
help: false,
},
CommandHint {
insert: "language rust",
display: "/language rust",
desc_key: "cmd_lang",
keep_open: false,
help: false,
},
CommandHint {
insert: "ui en",
display: "/ui en",
desc_key: "cmd_ui",
keep_open: false,
help: true,
},
CommandHint {
insert: "ui ko",
display: "/ui ko",
desc_key: "cmd_ui",
keep_open: false,
help: false,
},
CommandHint {
insert: "ui ja",
display: "/ui ja",
desc_key: "cmd_ui",
keep_open: false,
help: false,
},
CommandHint {
insert: "ui zh",
display: "/ui zh",
desc_key: "cmd_ui",
keep_open: false,
help: false,
},
CommandHint {
insert: "ui es",
display: "/ui es",
desc_key: "cmd_ui",
keep_open: false,
help: false,
},
CommandHint {
insert: "theme dark",
display: "/theme dark",
desc_key: "cmd_theme",
keep_open: false,
help: true,
},
CommandHint {
insert: "theme light",
display: "/theme light",
desc_key: "cmd_theme",
keep_open: false,
help: false,
},
CommandHint {
insert: "update",
display: "/update",
desc_key: "cmd_update",
keep_open: false,
help: true,
},
CommandHint {
insert: "help",
display: "/help",
desc_key: "cmd_help",
keep_open: false,
help: true,
},
CommandHint {
insert: "exit",
display: "/exit",
desc_key: "cmd_exit",
keep_open: false,
help: true,
},
];
+4
-0

@@ -35,2 +35,6 @@ {

"cmd_ai": "Ask AI about the current problem and code",
"cmd_profile": "Show practice profile",
"cmd_difficulty": "Set preferred difficulty",
"cmd_topics": "Set preferred topics",
"cmd_avoid": "Set topics to avoid",
"cmd_provider": "Set AI provider",

@@ -37,0 +41,0 @@ "cmd_model": "Set AI model",

@@ -35,2 +35,6 @@ {

"cmd_ai": "Preguntar a AI sobre el problema y codigo actuales",
"cmd_profile": "Mostrar perfil de practica",
"cmd_difficulty": "Configurar dificultad preferida",
"cmd_topics": "Configurar temas preferidos",
"cmd_avoid": "Configurar temas a evitar",
"cmd_provider": "Configurar AI provider",

@@ -37,0 +41,0 @@ "cmd_model": "Configurar AI model",

@@ -35,2 +35,6 @@ {

"cmd_ai": "現在の問題とコードについて AI に質問",
"cmd_profile": "練習プロファイルを表示",
"cmd_difficulty": "希望難易度を設定",
"cmd_topics": "希望トピックを設定",
"cmd_avoid": "避けるトピックを設定",
"cmd_provider": "AI provider を設定",

@@ -37,0 +41,0 @@ "cmd_model": "AI model を設定",

@@ -35,2 +35,6 @@ {

"cmd_ai": "현재 문제와 코드에 대해 AI에게 질문",
"cmd_profile": "연습 프로파일 보기",
"cmd_difficulty": "선호 난이도 설정",
"cmd_topics": "선호 주제 설정",
"cmd_avoid": "피할 주제 설정",
"cmd_provider": "AI provider 설정",

@@ -37,0 +41,0 @@ "cmd_model": "AI model 설정",

@@ -35,2 +35,6 @@ {

"cmd_ai": "向 AI 询问当前题目和代码",
"cmd_profile": "显示练习配置",
"cmd_difficulty": "设置偏好难度",
"cmd_topics": "设置偏好主题",
"cmd_avoid": "设置要避开的主题",
"cmd_provider": "设置 AI provider",

@@ -37,0 +41,0 @@ "cmd_model": "设置 AI model",

+33
-21
<svg width="1200" height="720" viewBox="0 0 1200 720" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="720" fill="#070b12"/>
<rect x="64" y="52" width="1072" height="616" rx="10" fill="#0b111a" stroke="#1f6f7a" stroke-width="2"/>
<rect x="88" y="78" width="594" height="440" rx="4" fill="#0f1720" stroke="#00c2d1" stroke-width="2"/>
<rect x="704" y="78" width="408" height="440" rx="4" fill="#0d141c" stroke="#31536b" stroke-width="2"/>
<text x="112" y="112" fill="#d7e1ec" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="18" font-weight="700">Problem</text>
<text x="728" y="112" fill="#d7e1ec" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="18" font-weight="700">solution.py</text>
<text x="112" y="158" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="22" font-weight="700">001. Hello World</text>
<text x="112" y="204" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">Difficulty: easy</text>
<text x="112" y="232" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">Topics: io</text>
<text x="112" y="286" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">Print exactly Hello, World! to stdout.</text>
<text x="112" y="352" fill="#00a8ff" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17" text-decoration="underline">Input</text>
<text x="112" y="386" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">No input.</text>
<text x="112" y="442" fill="#00a8ff" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17" text-decoration="underline">Output</text>
<text x="112" y="476" fill="#f6c177" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">Hello, World!</text>
<text x="728" y="158" fill="#6b7280" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17"> 1</text>
<text x="776" y="158" fill="#7dd3fc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">print</text>
<text x="828" y="158" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">('Hello, World!')</text>
<rect x="64" y="536" width="1072" height="28" fill="#152033"/>
<text x="84" y="556" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">PRACTICODE | 001-hello-world | easy | idle | solved | code:written | python | next:ai | ai:codex/auto</text>
<rect x="64" y="586" width="1072" height="58" rx="2" fill="#0b1017" stroke="#008cff" stroke-width="2"/>
<text x="84" y="606" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">Command</text>
<text x="84" y="630" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="18">/ai hint about edge cases</text>
<rect x="54" y="42" width="1092" height="636" rx="10" fill="#0b111a" stroke="#214c5c" stroke-width="2"/>
<rect x="76" y="68" width="560" height="430" rx="4" fill="#0f1720" stroke="#00c2d1" stroke-width="2"/>
<rect x="664" y="68" width="458" height="430" rx="4" fill="#0d141c" stroke="#f8e71c" stroke-width="2"/>
<text x="92" y="91" fill="#16d6e8" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">Problem</text>
<text x="680" y="91" fill="#f8e71c" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">&gt; solution.py</text>
<text x="96" y="128" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="22" font-weight="700">001. Hello World</text>
<text x="96" y="164" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">Difficulty: easy Topics: io</text>
<text x="96" y="206" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17">Print exactly Hello, World! to stdout.</text>
<text x="96" y="286" fill="#00d2e8" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17" font-weight="700">Input</text>
<rect x="96" y="304" width="486" height="44" rx="3" fill="#101d28" stroke="#29495a"/>
<text x="112" y="332" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">No input.</text>
<text x="96" y="388" fill="#00d2e8" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="17" font-weight="700">Output</text>
<rect x="96" y="406" width="486" height="66" rx="3" fill="#101d28" stroke="#29495a"/>
<text x="112" y="446" fill="#f6c177" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">Hello, World!</text>
<text x="690" y="128" fill="#64748b" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16"> 1</text>
<text x="738" y="128" fill="#7dd3fc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">print</text>
<text x="790" y="128" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="16">('Hello, World!')</text>
<rect x="76" y="512" width="1046" height="30" fill="#152033"/>
<text x="94" y="533" fill="#c8d3f5" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">PRACTICODE | 001-hello-world | easy | idle | assigned | code:written | python | Esc then / command</text>
<rect x="76" y="558" width="1046" height="48" rx="2" fill="#0b1017" stroke="#00c2d1" stroke-width="2"/>
<text x="94" y="579" fill="#16d6e8" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15" font-weight="700">Command</text>
<text x="94" y="596" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="18">/</text>
<rect x="76" y="614" width="1046" height="46" rx="2" fill="#101923" stroke="#214c5c"/>
<text x="94" y="641" fill="#f8fafc" font-family="SFMono-Regular, Menlo, Consolas, monospace" font-size="15">/run /next /hint &lt;request&gt; /profile /difficulty auto /topics arrays, strings</text>
</svg>

@@ -360,3 +360,3 @@ # This file is automatically @generated by Cargo.

name = "practicode"
version = "0.1.5"
version = "0.1.6"
dependencies = [

@@ -363,0 +363,0 @@ "anyhow",

[package]
name = "practicode"
version = "0.1.5"
version = "0.1.6"
edition = "2024"

@@ -5,0 +5,0 @@ description = "Local-first coding-test practice in a Rust terminal UI with optional AI help."

@@ -12,2 +12,3 @@ # Contributing

- For larger UI, AI-generation, storage, or packaging changes, open an issue first so the scope is clear.
- Check [ARCHITECTURE.md](ARCHITECTURE.md) before adding commands, settings, provider behavior, or persisted state.
- Do not commit local practice data from `.practicode/`, `problems/`, or `submissions/`.

@@ -14,0 +15,0 @@ - Do not include secrets, tokens, private prompts, or generated answer keys in docs or examples.

{
"name": "practicode",
"version": "0.1.5",
"version": "0.1.6",
"description": "Local-first coding-test practice in a Rust terminal UI with optional AI help.",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -69,13 +69,15 @@ # practicode

| `/next easy string problem` | Ask AI for a custom next problem |
| `/prev` | Go back through problem history |
| `/list` | Browse problems with `up/down` or `j/k`, open with `Enter` |
| `/back` | Go back through problem history |
| `/problems` | Browse problems with `up/down` or `j/k`, open with `Enter` |
| `/open 2` | Open by number, id, or slug |
| `/giveup` | Show the reference answer |
| `/answer` | Show the reference answer |
| `/hint` | Ask the selected AI for a concise hint |
| `/hint explain my bug` | Ask the selected AI about the current problem and submission |
| `/profile` | Show your current practice profile |
| `/difficulty auto` | Set difficulty preference: `auto`, `easy`, `medium`, `hard` |
| `/topics arrays, strings` | Set preferred topics for future problems |
| `/avoid dp, graph` | Set topics to avoid in future problems |
| `/provider codex` | Set AI provider and show local CLI/daemon status |
| `/model auto` | Use the provider default model for `/hint` and AI-backed `/next` |
| `/note prefer hashmap practice` | Append a standing note for future problem generation |
| `/notes` | Show your local next-problem notes |
| `/lang python` | Set code language: `python`, `ts`, `java`, `rust` |
| `/language python` | Set code language: `python`, `ts`, `java`, `rust` |
| `/ui en` | Set UI language: `en`, `ko`, `ja`, `zh`, `es` |

@@ -86,4 +88,8 @@ | `/theme dark` | Set theme: `dark` or `light` |

Older command names such as `/prev`, `/list`, `/giveup`, and `/lang` still work as aliases.
The default UI language is English. Switch it any time with `/ui ko`, `/ui ja`, `/ui zh`, or `/ui es`.
Your practice profile is saved in `.practicode/problem-state.json`. It keeps UI language, code language, theme, preferred difficulty, preferred topics, and topics to avoid. `auto` difficulty follows gradual progression; a fixed difficulty asks local selection and AI generation to prefer that level.
## AI Problems

@@ -144,4 +150,6 @@

Code layout and extension boundaries live in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
## License
practicode is MIT licensed. Third-party dependency license notes are in [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md).

@@ -126,4 +126,8 @@ use crate::{

pub fn default_ai_next_prompt(request: &str) -> String {
default_ai_next_prompt_with_settings(&Settings::default(), request)
}
pub fn default_ai_next_prompt_with_settings(settings: &Settings, request: &str) -> String {
format!(
"Read AGENTS.md, docs/problem-authoring-notes.md if present, .practicode/problem_notes.md if present, problems/INDEX.md if present, .practicode/problem_bank.json if present, and .practicode/problem-state.json. Create exactly one new non-duplicate coding practice problem. The built-in 001-hello-world already exists, so do not duplicate it. User request: {}. Make the smallest valid edits: update .practicode/problem_bank.json, one problem directory, problems/INDEX.md, and .practicode/problem-state.json. Do not include the answer in the problem statement.",
"Read AGENTS.md, docs/problem-authoring-notes.md if present, .practicode/problem_notes.md if present, problems/INDEX.md if present, .practicode/problem_bank.json if present, and .practicode/problem-state.json. Create exactly one new non-duplicate coding practice problem. The built-in 001-hello-world already exists, so do not duplicate it. User request: {}. Practice profile: difficulty preference: {}; preferred topics: {}; avoid topics: {}; code language: {}; UI language: {}. Treat difficulty auto as gradual progression from state; otherwise prefer the requested difficulty unless the direct user request conflicts. Make the smallest valid edits: update .practicode/problem_bank.json, one problem directory, problems/INDEX.md, and .practicode/problem-state.json. Do not include the answer in the problem statement.",
if request.is_empty() {

@@ -133,3 +137,8 @@ "(none)"

request
}
},
settings.difficulty,
list_or_none(&settings.topics),
list_or_none(&settings.avoid_topics),
settings.language,
settings.ui_language
)

@@ -309,3 +318,5 @@ }

exec.push(' ');
exec.push_str(&sh_quote(&default_ai_next_prompt(request)));
exec.push_str(&sh_quote(&default_ai_next_prompt_with_settings(
settings, request,
)));
format!("{start}; {exec}")

@@ -328,3 +339,5 @@ }

claude.push_str(" -p ");
claude.push_str(&sh_quote(&default_ai_next_prompt(request)));
claude.push_str(&sh_quote(&default_ai_next_prompt_with_settings(
settings, request,
)));
format!(

@@ -345,2 +358,10 @@ "claude daemon status >/dev/null 2>&1 || true; cd {}; {}",

fn list_or_none(values: &[String]) -> String {
if values.is_empty() {
"(none)".to_string()
} else {
values.join(", ")
}
}
#[cfg(test)]

@@ -347,0 +368,0 @@ mod tests {

@@ -12,2 +12,7 @@ use crate::process::{CommandSpec, run_capture, which};

mod profile;
pub use profile::{
DIFFICULTIES, default_difficulty, normalize_difficulty, normalize_topic_list, parse_topic_list,
};
pub const LANGUAGES: &[&str] = &["python", "ts", "java", "rust"];

@@ -29,2 +34,8 @@ pub use crate::i18n::{UI_LANGUAGES, normalize_ui_language, ui_text};

pub theme: String,
#[serde(default = "default_difficulty")]
pub difficulty: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub topics: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub avoid_topics: Vec<String>,
#[serde(default = "default_editor")]

@@ -48,2 +59,5 @@ pub editor: String,

theme: default_theme(),
difficulty: default_difficulty(),
topics: Vec::new(),
avoid_topics: Vec::new(),
editor: default_editor(),

@@ -370,2 +384,5 @@ next_source: default_next_source(),

}
settings.difficulty = normalize_difficulty(&settings.difficulty);
settings.topics = normalize_topic_list(&settings.topics);
settings.avoid_topics = normalize_topic_list(&settings.avoid_topics);
settings.next_source = normalize_next_source(&settings.next_source);

@@ -383,4 +400,5 @@ settings.ai_provider = normalize_ai_provider(&settings.ai_provider);

pub fn normalize_language(language: &str) -> String {
if LANGUAGES.contains(&language) {
language.to_string()
let language = language.trim().to_lowercase();
if LANGUAGES.contains(&language.as_str()) {
language
} else {

@@ -764,3 +782,7 @@ "python".to_string()

.collect::<Vec<_>>();
let preferred = &state.suggested_next_difficulty;
let preferred = if state.settings.difficulty == "auto" {
&state.suggested_next_difficulty
} else {
&state.settings.difficulty
};
let problem = bank

@@ -767,0 +789,0 @@ .iter()

Sorry, the diff of this file is too big to display