Skip to main content

j_cli/command/chat/
handler.rs

1use super::app::{CONFIG_FIELDS, CONFIG_GLOBAL_FIELDS, ChatApp, ChatMode, config_total_fields};
2use super::model::{ModelProvider, save_agent_config, save_chat_session};
3use super::render::copy_to_clipboard;
4use super::theme::ThemeName;
5use super::ui::draw_chat_ui;
6use crate::{error, info};
7use crossterm::{
8    event::{
9        self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers,
10        MouseEventKind,
11    },
12    execute,
13    terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
14};
15use ratatui::{Terminal, backend::CrosstermBackend};
16use std::io;
17
18pub fn run_chat_tui() {
19    match run_chat_tui_internal() {
20        Ok(_) => {}
21        Err(e) => {
22            error!("❌ Chat TUI 启动失败: {}", e);
23        }
24    }
25}
26
27pub fn run_chat_tui_internal() -> io::Result<()> {
28    terminal::enable_raw_mode()?;
29    let mut stdout = io::stdout();
30    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
31
32    let backend = CrosstermBackend::new(stdout);
33    let mut terminal = Terminal::new(backend)?;
34
35    let mut app = ChatApp::new();
36
37    if app.agent_config.providers.is_empty() {
38        terminal::disable_raw_mode()?;
39        execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
40        info!("⚠️  尚未配置 LLM 模型提供方,请先运行 j chat 查看配置说明。");
41        return Ok(());
42    }
43
44    let mut needs_redraw = true; // 首次必须绘制
45
46    loop {
47        // 清理过期 toast(如果有 toast 被清理,需要重绘)
48        let had_toast = app.toast.is_some();
49        app.tick_toast();
50        if had_toast && app.toast.is_none() {
51            needs_redraw = true;
52        }
53
54        // 非阻塞地处理后台流式消息
55        let was_loading = app.is_loading;
56        app.poll_stream();
57        // 流式加载中使用节流策略:只在内容增长超过阈值或超时才重绘
58        if app.is_loading {
59            let current_len = app.streaming_content.lock().unwrap().len();
60            let bytes_delta = current_len.saturating_sub(app.last_rendered_streaming_len);
61            let time_elapsed = app.last_stream_render_time.elapsed();
62            // 每增加 200 字节或距离上次渲染超过 200ms 才重绘
63            if bytes_delta >= 200
64                || time_elapsed >= std::time::Duration::from_millis(200)
65                || current_len == 0
66            {
67                needs_redraw = true;
68            }
69        } else if was_loading {
70            // 加载刚结束时必须重绘一次
71            needs_redraw = true;
72        }
73
74        // 只在状态发生变化时才重绘,大幅降低 CPU 占用
75        if needs_redraw {
76            terminal.draw(|f| draw_chat_ui(f, &mut app))?;
77            needs_redraw = false;
78            // 更新流式节流状态
79            if app.is_loading {
80                app.last_rendered_streaming_len = app.streaming_content.lock().unwrap().len();
81                app.last_stream_render_time = std::time::Instant::now();
82            }
83        }
84
85        // 等待事件:加载中用短间隔以刷新流式内容,空闲时用长间隔节省 CPU
86        let poll_timeout = if app.is_loading {
87            std::time::Duration::from_millis(150)
88        } else {
89            std::time::Duration::from_millis(1000)
90        };
91
92        if event::poll(poll_timeout)? {
93            // 批量消费所有待处理事件,避免快速滚动/打字时事件堆积
94            let mut should_break = false;
95            loop {
96                let evt = event::read()?;
97                match evt {
98                    Event::Key(key) => {
99                        needs_redraw = true;
100                        match app.mode {
101                            ChatMode::Chat => {
102                                if handle_chat_mode(&mut app, key) {
103                                    should_break = true;
104                                    break;
105                                }
106                            }
107                            ChatMode::SelectModel => handle_select_model(&mut app, key),
108                            ChatMode::Browse => handle_browse_mode(&mut app, key),
109                            ChatMode::Help => {
110                                app.mode = ChatMode::Chat;
111                            }
112                            ChatMode::Config => handle_config_mode(&mut app, key),
113                        }
114                    }
115                    Event::Mouse(mouse) => match mouse.kind {
116                        MouseEventKind::ScrollUp => {
117                            app.scroll_up();
118                            needs_redraw = true;
119                        }
120                        MouseEventKind::ScrollDown => {
121                            app.scroll_down();
122                            needs_redraw = true;
123                        }
124                        _ => {}
125                    },
126                    Event::Resize(_, _) => {
127                        needs_redraw = true;
128                    }
129                    _ => {}
130                }
131                // 继续消费剩余事件(非阻塞,Duration::ZERO)
132                if !event::poll(std::time::Duration::ZERO)? {
133                    break;
134                }
135            }
136            if should_break {
137                break;
138            }
139        }
140    }
141
142    // 保存对话历史
143    let _ = save_chat_session(&app.session);
144
145    terminal::disable_raw_mode()?;
146    execute!(
147        terminal.backend_mut(),
148        LeaveAlternateScreen,
149        DisableMouseCapture
150    )?;
151    Ok(())
152}
153
154/// 绘制 TUI 界面
155
156pub fn handle_chat_mode(app: &mut ChatApp, key: KeyEvent) -> bool {
157    // Ctrl+C 强制退出
158    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
159        return true;
160    }
161
162    // Ctrl+T 切换模型(替代 Ctrl+M,因为 Ctrl+M 在终端中等于 Enter)
163    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('t') {
164        if !app.agent_config.providers.is_empty() {
165            app.mode = ChatMode::SelectModel;
166            app.model_list_state
167                .select(Some(app.agent_config.active_index));
168        }
169        return false;
170    }
171
172    // Ctrl+L 清空对话
173    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('l') {
174        app.clear_session();
175        return false;
176    }
177
178    // Ctrl+Y 复制最后一条 AI 回复
179    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('y') {
180        if let Some(last_ai) = app
181            .session
182            .messages
183            .iter()
184            .rev()
185            .find(|m| m.role == "assistant")
186        {
187            if copy_to_clipboard(&last_ai.content) {
188                app.show_toast("已复制最后一条 AI 回复", false);
189            } else {
190                app.show_toast("复制到剪切板失败", true);
191            }
192        } else {
193            app.show_toast("暂无 AI 回复可复制", true);
194        }
195        return false;
196    }
197
198    // Ctrl+B 进入消息浏览模式(可选中历史消息并复制)
199    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('b') {
200        if !app.session.messages.is_empty() {
201            // 默认选中最后一条消息
202            app.browse_msg_index = app.session.messages.len() - 1;
203            app.mode = ChatMode::Browse;
204            app.msg_lines_cache = None; // 清除缓存以触发高亮重绘
205        } else {
206            app.show_toast("暂无消息可浏览", true);
207        }
208        return false;
209    }
210
211    // Ctrl+E 打开配置界面
212    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('e') {
213        // 初始化配置界面状态
214        app.config_provider_idx = app
215            .agent_config
216            .active_index
217            .min(app.agent_config.providers.len().saturating_sub(1));
218        app.config_field_idx = 0;
219        app.config_editing = false;
220        app.config_edit_buf.clear();
221        app.mode = ChatMode::Config;
222        return false;
223    }
224
225    // Ctrl+S 切换流式/非流式输出
226    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s') {
227        app.agent_config.stream_mode = !app.agent_config.stream_mode;
228        let _ = save_agent_config(&app.agent_config);
229        let mode_str = if app.agent_config.stream_mode {
230            "流式输出"
231        } else {
232            "整体输出"
233        };
234        app.show_toast(&format!("已切换为: {}", mode_str), false);
235        return false;
236    }
237
238    let char_count = app.input.chars().count();
239
240    match key.code {
241        KeyCode::Esc => return true,
242
243        KeyCode::Enter => {
244            if !app.is_loading {
245                app.send_message();
246            }
247        }
248
249        // 滚动消息
250        KeyCode::Up => app.scroll_up(),
251        KeyCode::Down => app.scroll_down(),
252        KeyCode::PageUp => {
253            for _ in 0..10 {
254                app.scroll_up();
255            }
256        }
257        KeyCode::PageDown => {
258            for _ in 0..10 {
259                app.scroll_down();
260            }
261        }
262
263        // 光标移动
264        KeyCode::Left => {
265            if app.cursor_pos > 0 {
266                app.cursor_pos -= 1;
267            }
268        }
269        KeyCode::Right => {
270            if app.cursor_pos < char_count {
271                app.cursor_pos += 1;
272            }
273        }
274        KeyCode::Home => app.cursor_pos = 0,
275        KeyCode::End => app.cursor_pos = char_count,
276
277        // 删除
278        KeyCode::Backspace => {
279            if app.cursor_pos > 0 {
280                let start = app
281                    .input
282                    .char_indices()
283                    .nth(app.cursor_pos - 1)
284                    .map(|(i, _)| i)
285                    .unwrap_or(0);
286                let end = app
287                    .input
288                    .char_indices()
289                    .nth(app.cursor_pos)
290                    .map(|(i, _)| i)
291                    .unwrap_or(app.input.len());
292                app.input.drain(start..end);
293                app.cursor_pos -= 1;
294            }
295        }
296        KeyCode::Delete => {
297            if app.cursor_pos < char_count {
298                let start = app
299                    .input
300                    .char_indices()
301                    .nth(app.cursor_pos)
302                    .map(|(i, _)| i)
303                    .unwrap_or(app.input.len());
304                let end = app
305                    .input
306                    .char_indices()
307                    .nth(app.cursor_pos + 1)
308                    .map(|(i, _)| i)
309                    .unwrap_or(app.input.len());
310                app.input.drain(start..end);
311            }
312        }
313
314        // F1 任何时候都能唤起帮助
315        KeyCode::F(1) => {
316            app.mode = ChatMode::Help;
317        }
318        // 输入框为空时,? 也可唤起帮助
319        KeyCode::Char('?') if app.input.is_empty() => {
320            app.mode = ChatMode::Help;
321        }
322        KeyCode::Char(c) => {
323            let byte_idx = app
324                .input
325                .char_indices()
326                .nth(app.cursor_pos)
327                .map(|(i, _)| i)
328                .unwrap_or(app.input.len());
329            app.input.insert_str(byte_idx, &c.to_string());
330            app.cursor_pos += 1;
331        }
332
333        _ => {}
334    }
335
336    false
337}
338
339/// 消息浏览模式按键处理:↑↓ 选择消息,y/Enter 复制选中消息,Esc 退出
340pub fn handle_browse_mode(app: &mut ChatApp, key: KeyEvent) {
341    let msg_count = app.session.messages.len();
342    if msg_count == 0 {
343        app.mode = ChatMode::Chat;
344        app.msg_lines_cache = None;
345        return;
346    }
347
348    match key.code {
349        KeyCode::Esc => {
350            app.mode = ChatMode::Chat;
351            app.msg_lines_cache = None; // 退出浏览模式时清除缓存,去掉高亮
352        }
353        KeyCode::Up | KeyCode::Char('k') => {
354            if app.browse_msg_index > 0 {
355                app.browse_msg_index -= 1;
356                app.msg_lines_cache = None; // 选中变化时清缓存
357            }
358        }
359        KeyCode::Down | KeyCode::Char('j') => {
360            if app.browse_msg_index < msg_count - 1 {
361                app.browse_msg_index += 1;
362                app.msg_lines_cache = None; // 选中变化时清缓存
363            }
364        }
365        KeyCode::Enter | KeyCode::Char('y') => {
366            // 复制选中消息的原始内容到剪切板
367            if let Some(msg) = app.session.messages.get(app.browse_msg_index) {
368                let content = msg.content.clone();
369                let role_label = if msg.role == "assistant" {
370                    "AI"
371                } else if msg.role == "user" {
372                    "用户"
373                } else {
374                    "系统"
375                };
376                if copy_to_clipboard(&content) {
377                    app.show_toast(
378                        &format!("已复制第 {} 条{}消息", app.browse_msg_index + 1, role_label),
379                        false,
380                    );
381                } else {
382                    app.show_toast("复制到剪切板失败", true);
383                }
384            }
385        }
386        _ => {}
387    }
388}
389
390/// 获取配置界面中当前字段的标签
391pub fn config_field_label(idx: usize) -> &'static str {
392    let total_provider = CONFIG_FIELDS.len();
393    if idx < total_provider {
394        match CONFIG_FIELDS[idx] {
395            "name" => "显示名称",
396            "api_base" => "API Base",
397            "api_key" => "API Key",
398            "model" => "模型名称",
399            _ => CONFIG_FIELDS[idx],
400        }
401    } else {
402        let gi = idx - total_provider;
403        match CONFIG_GLOBAL_FIELDS[gi] {
404            "system_prompt" => "系统提示词",
405            "stream_mode" => "流式输出",
406            "max_history_messages" => "历史消息数",
407            "theme" => "主题风格",
408            _ => CONFIG_GLOBAL_FIELDS[gi],
409        }
410    }
411}
412
413/// 获取配置界面中当前字段的值
414pub fn config_field_value(app: &ChatApp, field_idx: usize) -> String {
415    let total_provider = CONFIG_FIELDS.len();
416    if field_idx < total_provider {
417        if app.agent_config.providers.is_empty() {
418            return String::new();
419        }
420        let p = &app.agent_config.providers[app.config_provider_idx];
421        match CONFIG_FIELDS[field_idx] {
422            "name" => p.name.clone(),
423            "api_base" => p.api_base.clone(),
424            "api_key" => {
425                // 显示时隐藏 API Key 中间部分
426                if p.api_key.len() > 8 {
427                    format!(
428                        "{}****{}",
429                        &p.api_key[..4],
430                        &p.api_key[p.api_key.len() - 4..]
431                    )
432                } else {
433                    p.api_key.clone()
434                }
435            }
436            "model" => p.model.clone(),
437            _ => String::new(),
438        }
439    } else {
440        let gi = field_idx - total_provider;
441        match CONFIG_GLOBAL_FIELDS[gi] {
442            "system_prompt" => app.agent_config.system_prompt.clone().unwrap_or_default(),
443            "stream_mode" => {
444                if app.agent_config.stream_mode {
445                    "开启".into()
446                } else {
447                    "关闭".into()
448                }
449            }
450            "max_history_messages" => app.agent_config.max_history_messages.to_string(),
451            "theme" => app.agent_config.theme.display_name().to_string(),
452            _ => String::new(),
453        }
454    }
455}
456
457/// 获取配置字段的原始值(用于编辑时填入输入框)
458pub fn config_field_raw_value(app: &ChatApp, field_idx: usize) -> String {
459    let total_provider = CONFIG_FIELDS.len();
460    if field_idx < total_provider {
461        if app.agent_config.providers.is_empty() {
462            return String::new();
463        }
464        let p = &app.agent_config.providers[app.config_provider_idx];
465        match CONFIG_FIELDS[field_idx] {
466            "name" => p.name.clone(),
467            "api_base" => p.api_base.clone(),
468            "api_key" => p.api_key.clone(),
469            "model" => p.model.clone(),
470            _ => String::new(),
471        }
472    } else {
473        let gi = field_idx - total_provider;
474        match CONFIG_GLOBAL_FIELDS[gi] {
475            "system_prompt" => app.agent_config.system_prompt.clone().unwrap_or_default(),
476            "stream_mode" => {
477                if app.agent_config.stream_mode {
478                    "true".into()
479                } else {
480                    "false".into()
481                }
482            }
483            "theme" => app.agent_config.theme.to_str().to_string(),
484            _ => String::new(),
485        }
486    }
487}
488
489/// 将编辑结果写回配置
490pub fn config_field_set(app: &mut ChatApp, field_idx: usize, value: &str) {
491    let total_provider = CONFIG_FIELDS.len();
492    if field_idx < total_provider {
493        if app.agent_config.providers.is_empty() {
494            return;
495        }
496        let p = &mut app.agent_config.providers[app.config_provider_idx];
497        match CONFIG_FIELDS[field_idx] {
498            "name" => p.name = value.to_string(),
499            "api_base" => p.api_base = value.to_string(),
500            "api_key" => p.api_key = value.to_string(),
501            "model" => p.model = value.to_string(),
502            _ => {}
503        }
504    } else {
505        let gi = field_idx - total_provider;
506        match CONFIG_GLOBAL_FIELDS[gi] {
507            "system_prompt" => {
508                if value.is_empty() {
509                    app.agent_config.system_prompt = None;
510                } else {
511                    app.agent_config.system_prompt = Some(value.to_string());
512                }
513            }
514            "stream_mode" => {
515                app.agent_config.stream_mode = matches!(
516                    value.trim().to_lowercase().as_str(),
517                    "true" | "1" | "开启" | "on" | "yes"
518                );
519            }
520            "max_history_messages" => {
521                if let Ok(num) = value.trim().parse::<usize>() {
522                    app.agent_config.max_history_messages = num;
523                }
524            }
525            "theme" => {
526                app.agent_config.theme = ThemeName::from_str(value.trim());
527                app.theme = super::theme::Theme::from_name(&app.agent_config.theme);
528                app.msg_lines_cache = None;
529            }
530            _ => {}
531        }
532    }
533}
534
535/// 配置模式按键处理
536pub fn handle_config_mode(app: &mut ChatApp, key: KeyEvent) {
537    let total_fields = config_total_fields();
538
539    if app.config_editing {
540        // 正在编辑某个字段
541        match key.code {
542            KeyCode::Esc => {
543                // 取消编辑
544                app.config_editing = false;
545            }
546            KeyCode::Enter => {
547                // 确认编辑
548                let val = app.config_edit_buf.clone();
549                config_field_set(app, app.config_field_idx, &val);
550                app.config_editing = false;
551            }
552            KeyCode::Backspace => {
553                if app.config_edit_cursor > 0 {
554                    let idx = app
555                        .config_edit_buf
556                        .char_indices()
557                        .nth(app.config_edit_cursor - 1)
558                        .map(|(i, _)| i)
559                        .unwrap_or(0);
560                    let end_idx = app
561                        .config_edit_buf
562                        .char_indices()
563                        .nth(app.config_edit_cursor)
564                        .map(|(i, _)| i)
565                        .unwrap_or(app.config_edit_buf.len());
566                    app.config_edit_buf = format!(
567                        "{}{}",
568                        &app.config_edit_buf[..idx],
569                        &app.config_edit_buf[end_idx..]
570                    );
571                    app.config_edit_cursor -= 1;
572                }
573            }
574            KeyCode::Left => {
575                app.config_edit_cursor = app.config_edit_cursor.saturating_sub(1);
576            }
577            KeyCode::Right => {
578                let char_count = app.config_edit_buf.chars().count();
579                if app.config_edit_cursor < char_count {
580                    app.config_edit_cursor += 1;
581                }
582            }
583            KeyCode::Char(c) => {
584                let byte_idx = app
585                    .config_edit_buf
586                    .char_indices()
587                    .nth(app.config_edit_cursor)
588                    .map(|(i, _)| i)
589                    .unwrap_or(app.config_edit_buf.len());
590                app.config_edit_buf.insert(byte_idx, c);
591                app.config_edit_cursor += 1;
592            }
593            _ => {}
594        }
595        return;
596    }
597
598    // 非编辑状态
599    match key.code {
600        KeyCode::Esc => {
601            // 保存并返回
602            let _ = save_agent_config(&app.agent_config);
603            app.show_toast("配置已保存 ✅", false);
604            app.mode = ChatMode::Chat;
605        }
606        KeyCode::Up | KeyCode::Char('k') => {
607            if total_fields > 0 {
608                if app.config_field_idx == 0 {
609                    app.config_field_idx = total_fields - 1;
610                } else {
611                    app.config_field_idx -= 1;
612                }
613            }
614        }
615        KeyCode::Down | KeyCode::Char('j') => {
616            if total_fields > 0 {
617                app.config_field_idx = (app.config_field_idx + 1) % total_fields;
618            }
619        }
620        KeyCode::Tab | KeyCode::Right => {
621            // 切换 provider
622            let count = app.agent_config.providers.len();
623            if count > 1 {
624                app.config_provider_idx = (app.config_provider_idx + 1) % count;
625                // 切换后如果在 provider 字段区域,保持字段位置不变
626            }
627        }
628        KeyCode::BackTab | KeyCode::Left => {
629            // 反向切换 provider
630            let count = app.agent_config.providers.len();
631            if count > 1 {
632                if app.config_provider_idx == 0 {
633                    app.config_provider_idx = count - 1;
634                } else {
635                    app.config_provider_idx -= 1;
636                }
637            }
638        }
639        KeyCode::Enter => {
640            // 进入编辑模式
641            let total_provider = CONFIG_FIELDS.len();
642            if app.config_field_idx < total_provider && app.agent_config.providers.is_empty() {
643                app.show_toast("还没有 Provider,按 a 新增", true);
644                return;
645            }
646            // stream_mode 字段直接切换,不进入编辑模式
647            let gi = app.config_field_idx.checked_sub(total_provider);
648            if let Some(gi) = gi {
649                if CONFIG_GLOBAL_FIELDS[gi] == "stream_mode" {
650                    app.agent_config.stream_mode = !app.agent_config.stream_mode;
651                    return;
652                }
653                // theme 字段直接循环切换,不进入编辑模式
654                if CONFIG_GLOBAL_FIELDS[gi] == "theme" {
655                    app.switch_theme();
656                    return;
657                }
658            }
659            app.config_edit_buf = config_field_raw_value(app, app.config_field_idx);
660            app.config_edit_cursor = app.config_edit_buf.chars().count();
661            app.config_editing = true;
662        }
663        KeyCode::Char('a') => {
664            // 新增 Provider
665            let new_provider = ModelProvider {
666                name: format!("Provider-{}", app.agent_config.providers.len() + 1),
667                api_base: "https://api.openai.com/v1".to_string(),
668                api_key: String::new(),
669                model: String::new(),
670            };
671            app.agent_config.providers.push(new_provider);
672            app.config_provider_idx = app.agent_config.providers.len() - 1;
673            app.config_field_idx = 0; // 跳到 name 字段
674            app.show_toast("已新增 Provider,请填写配置", false);
675        }
676        KeyCode::Char('d') => {
677            // 删除当前 Provider
678            let count = app.agent_config.providers.len();
679            if count == 0 {
680                app.show_toast("没有可删除的 Provider", true);
681            } else {
682                let removed_name = app.agent_config.providers[app.config_provider_idx]
683                    .name
684                    .clone();
685                app.agent_config.providers.remove(app.config_provider_idx);
686                // 调整索引
687                if app.config_provider_idx >= app.agent_config.providers.len()
688                    && app.config_provider_idx > 0
689                {
690                    app.config_provider_idx -= 1;
691                }
692                // 调整 active_index
693                if app.agent_config.active_index >= app.agent_config.providers.len()
694                    && app.agent_config.active_index > 0
695                {
696                    app.agent_config.active_index -= 1;
697                }
698                app.show_toast(format!("已删除 Provider: {}", removed_name), false);
699            }
700        }
701        KeyCode::Char('s') => {
702            // 将当前 provider 设为活跃
703            if !app.agent_config.providers.is_empty() {
704                app.agent_config.active_index = app.config_provider_idx;
705                let name = app.agent_config.providers[app.config_provider_idx]
706                    .name
707                    .clone();
708                app.show_toast(format!("已设为活跃模型: {}", name), false);
709            }
710        }
711        _ => {}
712    }
713}
714
715/// 绘制配置编辑界面
716
717pub fn handle_select_model(app: &mut ChatApp, key: KeyEvent) {
718    let count = app.agent_config.providers.len();
719    match key.code {
720        KeyCode::Esc => {
721            app.mode = ChatMode::Chat;
722        }
723        KeyCode::Up | KeyCode::Char('k') => {
724            if count > 0 {
725                let i = app
726                    .model_list_state
727                    .selected()
728                    .map(|i| if i == 0 { count - 1 } else { i - 1 })
729                    .unwrap_or(0);
730                app.model_list_state.select(Some(i));
731            }
732        }
733        KeyCode::Down | KeyCode::Char('j') => {
734            if count > 0 {
735                let i = app
736                    .model_list_state
737                    .selected()
738                    .map(|i| if i >= count - 1 { 0 } else { i + 1 })
739                    .unwrap_or(0);
740                app.model_list_state.select(Some(i));
741            }
742        }
743        KeyCode::Enter => {
744            app.switch_model();
745        }
746        _ => {}
747    }
748}