practicode 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,225 @@
1
+ use super::*;
2
+
3
+ impl PracticodeApp {
4
+ pub(super) fn handle_key(&mut self, key: KeyEvent) -> Result<()> {
5
+ if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
6
+ self.should_quit = true;
7
+ return Ok(());
8
+ }
9
+ if self.handle_busy_key(key) {
10
+ return Ok(());
11
+ }
12
+ if self.editing_notes {
13
+ return self.handle_note_key(key);
14
+ }
15
+ match self.focus {
16
+ Focus::Command => self.handle_command_key(key),
17
+ Focus::Code => self.handle_code_key(key),
18
+ _ => self.handle_global_key(key),
19
+ }
20
+ }
21
+
22
+ pub(super) fn handle_mouse(&mut self, mouse: MouseEvent) -> Result<()> {
23
+ if self.task_rx.is_some() {
24
+ self.focus = Focus::Output;
25
+ return Ok(());
26
+ }
27
+ if !matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) {
28
+ return Ok(());
29
+ }
30
+ let position = Position::new(mouse.column, mouse.row);
31
+ if self.command_area.contains(position) {
32
+ self.focus_command();
33
+ } else if self.show_output && self.output_area.contains(position) {
34
+ self.focus = Focus::Output;
35
+ } else if self.code_area.contains(position) {
36
+ self.action_edit()?;
37
+ }
38
+ Ok(())
39
+ }
40
+
41
+ pub(super) fn handle_command_key(&mut self, key: KeyEvent) -> Result<()> {
42
+ match key.code {
43
+ KeyCode::Esc => {
44
+ self.command.clear();
45
+ self.command_cursor = 0;
46
+ self.command_palette_cursor = 0;
47
+ self.focus = Focus::None;
48
+ }
49
+ KeyCode::Enter => {
50
+ if !self.accept_command_palette()? {
51
+ let value = self.command.trim().to_string();
52
+ self.command.clear();
53
+ self.command_cursor = 0;
54
+ self.command_palette_cursor = 0;
55
+ self.focus = Focus::None;
56
+ self.submit_command(&value)?;
57
+ }
58
+ }
59
+ KeyCode::Backspace => self.delete_command_before_cursor(),
60
+ KeyCode::Delete => self.delete_command_at_cursor(),
61
+ KeyCode::Left => self.command_cursor = self.command_cursor.saturating_sub(1),
62
+ KeyCode::Right => {
63
+ self.command_cursor = (self.command_cursor + 1).min(char_len(&self.command));
64
+ }
65
+ KeyCode::Up => self.move_command_palette(-1),
66
+ KeyCode::Down => self.move_command_palette(1),
67
+ KeyCode::Home => self.command_cursor = 0,
68
+ KeyCode::End => self.command_cursor = char_len(&self.command),
69
+ KeyCode::Char('?') if self.command.trim().is_empty() || self.command.trim() == "/" => {
70
+ self.command.clear();
71
+ self.command_cursor = 0;
72
+ self.command_palette_cursor = 0;
73
+ self.focus = Focus::None;
74
+ self.handle_command("help")?;
75
+ }
76
+ KeyCode::Char(char) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
77
+ self.insert_command_char(char);
78
+ }
79
+ _ => {}
80
+ }
81
+ Ok(())
82
+ }
83
+
84
+ pub(super) fn handle_code_key(&mut self, key: KeyEvent) -> Result<()> {
85
+ match key.code {
86
+ KeyCode::Esc => self.focus = Focus::None,
87
+ KeyCode::Char(char) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
88
+ self.editor.insert_char(char);
89
+ self.save_code()?;
90
+ }
91
+ KeyCode::Enter => {
92
+ self.editor.insert_newline();
93
+ self.save_code()?;
94
+ }
95
+ KeyCode::Backspace => {
96
+ self.editor.backspace();
97
+ self.save_code()?;
98
+ }
99
+ KeyCode::Delete => {
100
+ self.editor.delete();
101
+ self.save_code()?;
102
+ }
103
+ KeyCode::Tab => {
104
+ for _ in 0..4 {
105
+ self.editor.insert_char(' ');
106
+ }
107
+ self.save_code()?;
108
+ }
109
+ KeyCode::Left => self.editor.move_left(),
110
+ KeyCode::Right => self.editor.move_right(),
111
+ KeyCode::Up => self.editor.move_up(),
112
+ KeyCode::Down => self.editor.move_down(),
113
+ _ => {}
114
+ }
115
+ Ok(())
116
+ }
117
+
118
+ pub(super) fn handle_note_key(&mut self, key: KeyEvent) -> Result<()> {
119
+ match key.code {
120
+ KeyCode::Esc => self.close_note_editor()?,
121
+ KeyCode::Char(char) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
122
+ self.note_editor.insert_char(char);
123
+ self.save_notes()?;
124
+ }
125
+ KeyCode::Enter => {
126
+ self.note_editor.insert_newline();
127
+ self.save_notes()?;
128
+ }
129
+ KeyCode::Backspace => {
130
+ self.note_editor.backspace();
131
+ self.save_notes()?;
132
+ }
133
+ KeyCode::Delete => {
134
+ self.note_editor.delete();
135
+ self.save_notes()?;
136
+ }
137
+ KeyCode::Tab => {
138
+ for _ in 0..4 {
139
+ self.note_editor.insert_char(' ');
140
+ }
141
+ self.save_notes()?;
142
+ }
143
+ KeyCode::Left => self.note_editor.move_left(),
144
+ KeyCode::Right => self.note_editor.move_right(),
145
+ KeyCode::Up => self.note_editor.move_up(),
146
+ KeyCode::Down => self.note_editor.move_down(),
147
+ _ => {}
148
+ }
149
+ Ok(())
150
+ }
151
+
152
+ pub(super) fn handle_global_key(&mut self, key: KeyEvent) -> Result<()> {
153
+ if self.settings_cursor.is_some() {
154
+ match key.code {
155
+ KeyCode::Up | KeyCode::Char('k') => self.move_settings_cursor(-1),
156
+ KeyCode::Down | KeyCode::Char('j') => self.move_settings_cursor(1),
157
+ KeyCode::Char(' ') | KeyCode::Enter => self.change_selected_setting()?,
158
+ KeyCode::Esc => {
159
+ self.settings_cursor = None;
160
+ self.show_output = false;
161
+ self.focus = Focus::Code;
162
+ }
163
+ _ => self.handle_global_shortcut(key)?,
164
+ }
165
+ return Ok(());
166
+ }
167
+ if let Some(cursor) = self.list_cursor {
168
+ match key.code {
169
+ KeyCode::Up | KeyCode::Char('k') => self.move_list_cursor(-1),
170
+ KeyCode::Down | KeyCode::Char('j') => self.move_list_cursor(1),
171
+ KeyCode::Enter => self.open_selected_problem()?,
172
+ KeyCode::Esc => {
173
+ self.list_cursor = None;
174
+ self.write_text_output("Closed list.");
175
+ }
176
+ _ => {
177
+ self.list_cursor = Some(cursor);
178
+ self.handle_global_shortcut(key)?;
179
+ }
180
+ }
181
+ return Ok(());
182
+ }
183
+ if key.code == KeyCode::Esc && self.show_output {
184
+ self.show_output = false;
185
+ self.focus = Focus::Code;
186
+ return Ok(());
187
+ }
188
+ self.handle_global_shortcut(key)
189
+ }
190
+
191
+ pub(super) fn handle_global_shortcut(&mut self, key: KeyEvent) -> Result<()> {
192
+ match key.code {
193
+ KeyCode::Char('/') => self.focus_command(),
194
+ KeyCode::Char('?') => self.handle_command("help")?,
195
+ KeyCode::Char('r') => self.action_run()?,
196
+ KeyCode::Char('n') => self.action_next("")?,
197
+ KeyCode::Char('p') => self.action_previous()?,
198
+ KeyCode::Char('g') => self.action_give_up()?,
199
+ KeyCode::Char('e') => self.action_edit()?,
200
+ KeyCode::Char('l') => self.action_cycle_language()?,
201
+ KeyCode::Char('u') => self.action_toggle_ui_language()?,
202
+ KeyCode::Char('q') => self.should_quit = true,
203
+ _ => {}
204
+ }
205
+ Ok(())
206
+ }
207
+
208
+ pub(super) fn focus_command(&mut self) {
209
+ if self.command.is_empty() {
210
+ self.command.push('/');
211
+ self.command_cursor = 1;
212
+ }
213
+ self.command_palette_cursor = 0;
214
+ self.focus = Focus::Command;
215
+ }
216
+
217
+ pub(super) fn submit_command(&mut self, value: &str) -> Result<()> {
218
+ let value = value
219
+ .trim()
220
+ .strip_prefix('/')
221
+ .unwrap_or(value.trim())
222
+ .trim();
223
+ self.handle_command(value)
224
+ }
225
+ }
@@ -0,0 +1,163 @@
1
+ use super::*;
2
+
3
+ impl PracticodeApp {
4
+ pub(super) fn load_code_editor(&mut self) -> Result<()> {
5
+ let path = ensure_submission(&self.root, &self.problem, &self.state.settings)?;
6
+ let text = fs::read_to_string(path).unwrap_or_default();
7
+ self.editor.set_text(&text);
8
+ Ok(())
9
+ }
10
+
11
+ pub(super) fn save_code(&self) -> Result<()> {
12
+ let path = ensure_submission(&self.root, &self.problem, &self.state.settings)?;
13
+ fs::write(path, self.editor.text())?;
14
+ Ok(())
15
+ }
16
+
17
+ pub(super) fn start_problem_list(&mut self) {
18
+ self.list_cursor = Some(self.current_problem_index());
19
+ self.write_text_output(&self.render_problem_list());
20
+ }
21
+
22
+ pub(super) fn render_problem_list(&self) -> String {
23
+ let status_by_id = self
24
+ .state
25
+ .history
26
+ .iter()
27
+ .map(|item| (item.id.as_str(), item.status.as_str()))
28
+ .collect::<HashMap<_, _>>();
29
+ let cursor = self
30
+ .list_cursor
31
+ .unwrap_or_else(|| self.current_problem_index());
32
+ let mut lines = vec![
33
+ "Problems".to_string(),
34
+ String::new(),
35
+ " # ID Difficulty Status Code Title".to_string(),
36
+ ];
37
+ for (index, problem) in self.bank.iter().enumerate() {
38
+ let marker = if index == cursor { ">" } else { " " };
39
+ let current = if problem.id == self.problem.id {
40
+ "*"
41
+ } else {
42
+ " "
43
+ };
44
+ let title = localized(&problem.title, &self.state.settings.ui_language);
45
+ let code_status = self.submission_status(problem).0;
46
+ lines.push(format!(
47
+ "{marker} {current} {:>2} {:<18} {:<10} {:<10} {:<9} {title}",
48
+ index + 1,
49
+ problem.id,
50
+ problem.difficulty,
51
+ status_by_id
52
+ .get(problem.id.as_str())
53
+ .copied()
54
+ .unwrap_or("-"),
55
+ code_status,
56
+ ));
57
+ }
58
+ lines.push("\nup/down or j/k select | enter open | esc close".to_string());
59
+ lines.join("\n")
60
+ }
61
+
62
+ pub(super) fn current_problem_index(&self) -> usize {
63
+ self.bank
64
+ .iter()
65
+ .position(|problem| problem.id == self.problem.id)
66
+ .unwrap_or(0)
67
+ }
68
+
69
+ pub(super) fn move_list_cursor(&mut self, delta: isize) {
70
+ if self.bank.is_empty() {
71
+ return;
72
+ }
73
+ let cursor = self
74
+ .list_cursor
75
+ .unwrap_or_else(|| self.current_problem_index()) as isize;
76
+ let len = self.bank.len() as isize;
77
+ self.list_cursor = Some(((cursor + delta).rem_euclid(len)) as usize);
78
+ self.write_text_output(&self.render_problem_list());
79
+ }
80
+
81
+ pub(super) fn open_selected_problem(&mut self) -> Result<()> {
82
+ if let Some(cursor) = self.list_cursor {
83
+ let problem_id = self.bank[cursor].id.clone();
84
+ self.list_cursor = None;
85
+ self.open_problem(&problem_id)?;
86
+ }
87
+ Ok(())
88
+ }
89
+
90
+ pub(super) fn open_problem(&mut self, query: &str) -> Result<()> {
91
+ self.list_cursor = None;
92
+ let Some(problem) = self.find_problem(query).cloned() else {
93
+ self.write_text_output(&format!("Problem not found: {query}\nTry /problems."));
94
+ return Ok(());
95
+ };
96
+ self.problem = problem;
97
+ self.state.current_problem = self.problem.id.clone();
98
+ if !self
99
+ .state
100
+ .history
101
+ .iter()
102
+ .any(|item| item.id == self.problem.id)
103
+ {
104
+ self.state.history.push(HistoryItem {
105
+ id: self.problem.id.clone(),
106
+ status: "assigned".to_string(),
107
+ });
108
+ }
109
+ save_state(&self.root, &self.state)?;
110
+ ensure_problem_files(&self.root, &self.problem)?;
111
+ self.load_code_editor()?;
112
+ self.show_output = false;
113
+ self.focus = Focus::Code;
114
+ Ok(())
115
+ }
116
+
117
+ pub(super) fn find_problem(&self, query: &str) -> Option<&Problem> {
118
+ let needle = if query.trim().chars().all(|c| c.is_ascii_digit()) {
119
+ format!("{:03}", query.trim().parse::<usize>().ok()?)
120
+ } else {
121
+ query.trim().to_lowercase()
122
+ };
123
+ self.bank.iter().find(|problem| {
124
+ needle == problem.id.to_lowercase()
125
+ || needle == problem.slug.to_lowercase()
126
+ || problem.id.starts_with(&needle)
127
+ })
128
+ }
129
+
130
+ pub(super) fn problem_status(&self, problem: &Problem) -> String {
131
+ if self.state.solved.contains(&problem.id) {
132
+ return "solved".to_string();
133
+ }
134
+ self.state
135
+ .history
136
+ .iter()
137
+ .rev()
138
+ .find(|item| item.id == problem.id)
139
+ .map(|item| item.status.clone())
140
+ .unwrap_or_else(|| "not_started".to_string())
141
+ }
142
+
143
+ pub(super) fn submission_status(&self, problem: &Problem) -> (String, String) {
144
+ let language = normalize_language(&self.state.settings.language);
145
+ let path = self
146
+ .root
147
+ .join("submissions")
148
+ .join(&problem.id)
149
+ .join(format!("solution.{}", ext_for(&language)));
150
+ if !path.exists() {
151
+ return ("missing".to_string(), format!("({language})"));
152
+ }
153
+ let content = fs::read_to_string(&path).unwrap_or_default();
154
+ let relative = path.strip_prefix(&self.root).unwrap_or(&path).display();
155
+ if content == template_for(&language) {
156
+ ("template".to_string(), format!("({relative})"))
157
+ } else if content.trim().is_empty() {
158
+ ("empty".to_string(), format!("({relative})"))
159
+ } else {
160
+ ("written".to_string(), format!("({relative})"))
161
+ }
162
+ }
163
+ }
@@ -1,14 +1,30 @@
1
- use crate::core::{AppState, DIFFICULTIES, LANGUAGES, THEMES, UI_LANGUAGES};
1
+ use crate::core::{
2
+ AI_PROVIDERS, AppState, CLAUDE_AI_EFFORTS, CODEX_AI_EFFORTS, DIFFICULTIES, LANGUAGES, THEMES,
3
+ UI_LANGUAGES, normalize_ai_effort,
4
+ };
2
5
 
3
6
  pub(super) struct SettingsChange {
4
7
  pub reload_editor: bool,
8
+ pub provider_changed: bool,
9
+ pub edit_notes: bool,
5
10
  }
6
11
 
12
+ const AI_PROVIDER_ROW: usize = 4;
13
+ pub(super) const AI_MODEL_ROW: usize = 5;
14
+ const AI_EFFORT_ROW: usize = 6;
15
+ const NOTE_ROW: usize = 7;
16
+ const TOGGLE_START: usize = 8;
17
+
7
18
  pub(super) fn row_count() -> usize {
8
- 4 + LANGUAGES.len() + UI_LANGUAGES.len()
19
+ TOGGLE_START + LANGUAGES.len() + UI_LANGUAGES.len()
9
20
  }
10
21
 
11
- pub(super) fn render(state: &AppState, cursor: Option<usize>) -> String {
22
+ pub(super) fn render(
23
+ state: &AppState,
24
+ cursor: Option<usize>,
25
+ available_models: &[String],
26
+ models_loading: bool,
27
+ ) -> String {
12
28
  let settings = &state.settings;
13
29
  let ui_language = settings.ui_language.as_str();
14
30
  let topics = list_or_none(&settings.topics, ui_language);
@@ -63,20 +79,52 @@ pub(super) fn render(state: &AppState, cursor: Option<usize>) -> String {
63
79
  "{}: {generate_ui_languages}",
64
80
  label(ui_language, "generated_ui_languages")
65
81
  ),
66
- format!("AI provider: {}", settings.ai_provider),
67
- format!(
68
- "AI model: {}",
69
- if settings.ai_model == "auto" {
70
- label(ui_language, "provider_default")
71
- } else {
72
- settings.ai_model.as_str()
73
- }
82
+ row(
83
+ cursor,
84
+ AI_PROVIDER_ROW,
85
+ &format!("AI provider: {}", settings.ai_provider),
86
+ ),
87
+ row(
88
+ cursor,
89
+ AI_MODEL_ROW,
90
+ &format!(
91
+ "AI model: {}{}",
92
+ if settings.ai_model == "auto" {
93
+ label(ui_language, "provider_default")
94
+ } else {
95
+ settings.ai_model.as_str()
96
+ },
97
+ if models_loading {
98
+ " (loading)"
99
+ } else if available_models.is_empty() {
100
+ " (/model to load)"
101
+ } else {
102
+ ""
103
+ }
104
+ ),
105
+ ),
106
+ row(
107
+ cursor,
108
+ AI_EFFORT_ROW,
109
+ &format!(
110
+ "AI effort: {}",
111
+ if settings.ai_effort == "auto" {
112
+ label(ui_language, "provider_default")
113
+ } else {
114
+ settings.ai_effort.as_str()
115
+ }
116
+ ),
117
+ ),
118
+ row(
119
+ cursor,
120
+ NOTE_ROW,
121
+ &format!("{}: Enter", label(ui_language, "problem_notes")),
74
122
  ),
75
123
  String::new(),
76
124
  label(ui_language, "answer_toggles").to_string(),
77
125
  ];
78
126
  for (index, language) in LANGUAGES.iter().enumerate() {
79
- let row_index = 4 + index;
127
+ let row_index = TOGGLE_START + index;
80
128
  let checked = generate_language_enabled(state, language);
81
129
  lines.push(row(
82
130
  cursor,
@@ -87,7 +135,7 @@ pub(super) fn render(state: &AppState, cursor: Option<usize>) -> String {
87
135
  lines.push(String::new());
88
136
  lines.push(label(ui_language, "ui_toggles").to_string());
89
137
  for (index, language) in UI_LANGUAGES.iter().enumerate() {
90
- let row_index = 4 + LANGUAGES.len() + index;
138
+ let row_index = TOGGLE_START + LANGUAGES.len() + index;
91
139
  let checked = generate_ui_language_enabled(state, language);
92
140
  lines.push(row(
93
141
  cursor,
@@ -106,10 +154,84 @@ pub(super) fn render(state: &AppState, cursor: Option<usize>) -> String {
106
154
  "/generate-ui all|en, ko".to_string(),
107
155
  "/provider codex|claude".to_string(),
108
156
  "/model auto".to_string(),
157
+ "/effort auto|low|medium|high|xhigh|max".to_string(),
158
+ "/note".to_string(),
159
+ "/notes".to_string(),
109
160
  ]);
110
161
  lines.join("\n")
111
162
  }
112
163
 
164
+ pub(super) fn apply_selected(
165
+ state: &mut AppState,
166
+ selected: usize,
167
+ available_models: &[String],
168
+ ) -> SettingsChange {
169
+ let mut change = SettingsChange {
170
+ reload_editor: false,
171
+ provider_changed: false,
172
+ edit_notes: false,
173
+ };
174
+ match selected {
175
+ 0 => {
176
+ let current = LANGUAGES
177
+ .iter()
178
+ .position(|language| language == &state.settings.language)
179
+ .unwrap_or(0);
180
+ state.settings.language = LANGUAGES[(current + 1) % LANGUAGES.len()].to_string();
181
+ change.reload_editor = true;
182
+ }
183
+ 1 => {
184
+ let current = UI_LANGUAGES
185
+ .iter()
186
+ .position(|language| language == &state.settings.ui_language)
187
+ .unwrap_or(0);
188
+ state.settings.ui_language =
189
+ UI_LANGUAGES[(current + 1) % UI_LANGUAGES.len()].to_string();
190
+ }
191
+ 2 => {
192
+ let current = THEMES
193
+ .iter()
194
+ .position(|theme| theme == &state.settings.theme)
195
+ .unwrap_or(0);
196
+ state.settings.theme = THEMES[(current + 1) % THEMES.len()].to_string();
197
+ }
198
+ 3 => {
199
+ let current = DIFFICULTIES
200
+ .iter()
201
+ .position(|difficulty| difficulty == &state.settings.difficulty)
202
+ .unwrap_or(0);
203
+ let difficulty = DIFFICULTIES[(current + 1) % DIFFICULTIES.len()].to_string();
204
+ state.settings.difficulty = difficulty.clone();
205
+ if difficulty != "auto" {
206
+ state.suggested_next_difficulty = difficulty;
207
+ }
208
+ }
209
+ AI_PROVIDER_ROW => {
210
+ let current = AI_PROVIDERS
211
+ .iter()
212
+ .position(|provider| provider == &state.settings.ai_provider)
213
+ .unwrap_or(0);
214
+ state.settings.ai_provider =
215
+ AI_PROVIDERS[(current + 1) % AI_PROVIDERS.len()].to_string();
216
+ state.settings.ai_model = "auto".to_string();
217
+ state.settings.ai_effort =
218
+ normalize_ai_effort(&state.settings.ai_provider, &state.settings.ai_effort);
219
+ change.provider_changed = true;
220
+ }
221
+ AI_MODEL_ROW => cycle_ai_model(state, available_models),
222
+ AI_EFFORT_ROW => cycle_ai_effort(state),
223
+ NOTE_ROW => change.edit_notes = true,
224
+ row if row < TOGGLE_START + LANGUAGES.len() => {
225
+ toggle_generate_language(state, LANGUAGES[row - TOGGLE_START]);
226
+ }
227
+ row if row < row_count() => {
228
+ toggle_generate_ui_language(state, UI_LANGUAGES[row - TOGGLE_START - LANGUAGES.len()]);
229
+ }
230
+ _ => {}
231
+ }
232
+ change
233
+ }
234
+
113
235
  fn label<'a>(ui_language: &str, key: &'a str) -> &'a str {
114
236
  if ui_language == "ko" {
115
237
  match key {
@@ -124,6 +246,7 @@ fn label<'a>(ui_language: &str, key: &'a str) -> &'a str {
124
246
  "generated_answer_languages" => "생성 정답 언어",
125
247
  "generated_ui_languages" => "생성 문제 언어",
126
248
  "provider_default" => "auto (provider 기본값)",
249
+ "problem_notes" => "문제 생성 메모 편집",
127
250
  "answer_toggles" => "생성 정답 언어 토글",
128
251
  "ui_toggles" => "생성 문제 언어 토글",
129
252
  "commands" => "명령",
@@ -144,6 +267,7 @@ fn label<'a>(ui_language: &str, key: &'a str) -> &'a str {
144
267
  "generated_answer_languages" => "Generated answer languages",
145
268
  "generated_ui_languages" => "Generated UI languages",
146
269
  "provider_default" => "auto (provider default)",
270
+ "problem_notes" => "Edit problem notes",
147
271
  "answer_toggles" => "Generated answer language toggles",
148
272
  "ui_toggles" => "Generated problem text language toggles",
149
273
  "commands" => "Commands",
@@ -154,52 +278,27 @@ fn label<'a>(ui_language: &str, key: &'a str) -> &'a str {
154
278
  }
155
279
  }
156
280
 
157
- pub(super) fn apply_selected(state: &mut AppState, selected: usize) -> SettingsChange {
158
- let mut reload_editor = false;
159
- match selected {
160
- 0 => {
161
- let current = LANGUAGES
162
- .iter()
163
- .position(|language| language == &state.settings.language)
164
- .unwrap_or(0);
165
- state.settings.language = LANGUAGES[(current + 1) % LANGUAGES.len()].to_string();
166
- reload_editor = true;
167
- }
168
- 1 => {
169
- let current = UI_LANGUAGES
170
- .iter()
171
- .position(|language| language == &state.settings.ui_language)
172
- .unwrap_or(0);
173
- state.settings.ui_language =
174
- UI_LANGUAGES[(current + 1) % UI_LANGUAGES.len()].to_string();
175
- }
176
- 2 => {
177
- let current = THEMES
178
- .iter()
179
- .position(|theme| theme == &state.settings.theme)
180
- .unwrap_or(0);
181
- state.settings.theme = THEMES[(current + 1) % THEMES.len()].to_string();
182
- }
183
- 3 => {
184
- let current = DIFFICULTIES
185
- .iter()
186
- .position(|difficulty| difficulty == &state.settings.difficulty)
187
- .unwrap_or(0);
188
- let difficulty = DIFFICULTIES[(current + 1) % DIFFICULTIES.len()].to_string();
189
- state.settings.difficulty = difficulty.clone();
190
- if difficulty != "auto" {
191
- state.suggested_next_difficulty = difficulty;
192
- }
193
- }
194
- row if row < 4 + LANGUAGES.len() => {
195
- toggle_generate_language(state, LANGUAGES[row - 4]);
196
- }
197
- row if row < row_count() => {
198
- toggle_generate_ui_language(state, UI_LANGUAGES[row - 4 - LANGUAGES.len()]);
199
- }
200
- _ => {}
201
- }
202
- SettingsChange { reload_editor }
281
+ fn cycle_ai_model(state: &mut AppState, available_models: &[String]) {
282
+ let mut models = vec!["auto"];
283
+ models.extend(available_models.iter().map(String::as_str));
284
+ let current = models
285
+ .iter()
286
+ .position(|model| model == &state.settings.ai_model)
287
+ .unwrap_or(0);
288
+ state.settings.ai_model = models[(current + 1) % models.len()].to_string();
289
+ }
290
+
291
+ fn cycle_ai_effort(state: &mut AppState) {
292
+ let efforts = if state.settings.ai_provider == "claude" {
293
+ CLAUDE_AI_EFFORTS
294
+ } else {
295
+ CODEX_AI_EFFORTS
296
+ };
297
+ let current = efforts
298
+ .iter()
299
+ .position(|effort| effort == &state.settings.ai_effort)
300
+ .unwrap_or(0);
301
+ state.settings.ai_effort = efforts[(current + 1) % efforts.len()].to_string();
203
302
  }
204
303
 
205
304
  fn row(cursor: Option<usize>, index: usize, text: &str) -> String {