返回 DeepSeek-TUI-2026
mod.rs
根目录 / crates / tui / src / tui / onboarding / mod.rs
1 //! Onboarding flow rendering and helpers.
2
3 pub mod api_key;
4 pub mod language;
5 pub mod trust_directory;
6 pub mod welcome;
7
8 use std::path::{Path, PathBuf};
9
10 use ratatui::{
11 Frame,
12 layout::Rect,
13 style::{Modifier, Style},
14 text::{Line, Span},
15 widgets::{Block, Borders, Padding, Paragraph, Wrap},
16 };
17
18 use crate::palette;
19 use crate::tui::app::{App, OnboardingState};
20
21 pub fn render(f: &mut Frame, area: Rect, app: &App) {
22 let block = Block::default().style(Style::default().bg(palette::DEEPSEEK_INK));
23 f.render_widget(block, area);
24
25 let content_width = 76.min(area.width.saturating_sub(4));
26 let content_height = 20.min(area.height.saturating_sub(4));
27 let content_area = Rect {
28 x: (area.width - content_width) / 2,
29 y: (area.height - content_height) / 2,
30 width: content_width,
31 height: content_height,
32 };
33
34 let lines = match app.onboarding {
35 OnboardingState::Welcome => welcome::lines(),
36 OnboardingState::Language => language::lines(app),
37 OnboardingState::ApiKey => api_key::lines(app),
38 OnboardingState::TrustDirectory => trust_directory::lines(app),
39 OnboardingState::Tips => tips_lines(),
40 OnboardingState::None => Vec::new(),
41 };
42
43 if !lines.is_empty() {
44 let mut panel = Block::default()
45 .title(Line::from(Span::styled(
46 " DeepSeek TUI ",
47 Style::default()
48 .fg(palette::DEEPSEEK_BLUE)
49 .add_modifier(Modifier::BOLD),
50 )))
51 .borders(Borders::ALL)
52 .border_style(Style::default().fg(palette::BORDER_COLOR))
53 .style(Style::default().bg(palette::DEEPSEEK_SLATE))
54 .padding(Padding::new(2, 2, 1, 1));
55 if !app.onboarding_workspace_trust_gate {
56 let (step, total) = onboarding_step(app);
57 panel = panel.title_bottom(Line::from(Span::styled(
58 format!(" Step {step}/{total} "),
59 Style::default()
60 .fg(palette::TEXT_MUTED)
61 .add_modifier(Modifier::BOLD),
62 )));
63 }
64 let inner = panel.inner(content_area);
65 f.render_widget(panel, content_area);
66 let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
67 f.render_widget(paragraph, inner);
68 }
69 }
70
71 fn onboarding_step(app: &App) -> (usize, usize) {
72 let needs_trust = !app.trust_mode && needs_trust(&app.workspace);
73 // Welcome + Language + Tips are always shown.
74 let mut total = 3;
75 if app.onboarding_needs_api_key {
76 total += 1;
77 }
78 if needs_trust {
79 total += 1;
80 }
81
82 let step = match app.onboarding {
83 OnboardingState::Welcome => 1,
84 OnboardingState::Language => 2,
85 OnboardingState::ApiKey => 3,
86 OnboardingState::TrustDirectory => {
87 // Welcome (1) + Language (2) + optional ApiKey
88 if app.onboarding_needs_api_key { 4 } else { 3 }
89 }
90 OnboardingState::Tips => total,
91 OnboardingState::None => total,
92 };
93
94 (step, total)
95 }
96
97 pub fn tips_lines() -> Vec<ratatui::text::Line<'static>> {
98 use ratatui::style::Modifier;
99 use ratatui::text::{Line, Span};
100
101 vec![
102 Line::from(Span::styled(
103 "Start Simple",
104 Style::default()
105 .fg(palette::DEEPSEEK_SKY)
106 .add_modifier(Modifier::BOLD),
107 )),
108 Line::from(""),
109 Line::from(Span::raw(
110 "Write the task in plain language. Use /help or Ctrl+K when you want a command.",
111 )),
112 Line::from(Span::raw(
113 "The bottom composer is multi-line: Enter sends, Alt+Enter or Ctrl+J adds a new line.",
114 )),
115 Line::from(Span::raw(
116 "Switch modes only when the job changes: Plan for review-first work, Agent for execution, YOLO when you want auto-approval.",
117 )),
118 Line::from(Span::raw(
119 "Ctrl+R resumes earlier sessions, and Esc backs out of the current draft or overlay.",
120 )),
121 Line::from(vec![
122 Span::styled("Press ", Style::default().fg(palette::TEXT_MUTED)),
123 Span::styled(
124 "Enter",
125 Style::default()
126 .fg(palette::TEXT_PRIMARY)
127 .add_modifier(Modifier::BOLD),
128 ),
129 Span::styled(
130 " to open the workspace",
131 Style::default().fg(palette::TEXT_MUTED),
132 ),
133 ]),
134 ]
135 }
136
137 pub fn default_marker_path() -> Option<PathBuf> {
138 dirs::home_dir().map(|home| home.join(".deepseek").join(".onboarded"))
139 }
140
141 pub fn is_onboarded() -> bool {
142 default_marker_path().is_some_and(|path| path.exists())
143 }
144
145 pub fn mark_onboarded() -> std::io::Result<PathBuf> {
146 let path = default_marker_path().ok_or_else(|| {
147 std::io::Error::new(std::io::ErrorKind::NotFound, "Home directory not found")
148 })?;
149 if let Some(parent) = path.parent() {
150 std::fs::create_dir_all(parent)?;
151 }
152 std::fs::write(&path, "")?;
153 Ok(path)
154 }
155
156 pub fn needs_trust(workspace: &Path) -> bool {
157 if crate::config::is_workspace_trusted(workspace) {
158 return false;
159 }
160
161 let markers = [
162 workspace.join(".deepseek").join("trusted"),
163 workspace.join(".deepseek").join("trust.json"),
164 ];
165 !markers.iter().any(|path| path.exists())
166 }
167
168 pub fn mark_trusted(workspace: &Path) -> anyhow::Result<PathBuf> {
169 crate::config::save_workspace_trust(workspace)
170 }
171
171 lines RUST