• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dustinblackman / oatmeal / 7269632354

20 Dec 2023 01:11AM UTC coverage: 45.041% (-0.6%) from 45.689%
7269632354

push

github

dustinblackman
refactor: Model boundaries

13 of 41 new or added lines in 9 files covered. (31.71%)

27 existing lines in 7 files now uncovered.

1108 of 2460 relevant lines covered (45.04%)

17.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/application/ui.rs
1
use std::io;
2

3
use anyhow::Result;
4
use crossterm::cursor;
5
use crossterm::event::DisableBracketedPaste;
6
use crossterm::event::DisableMouseCapture;
7
use crossterm::event::EnableBracketedPaste;
8
use crossterm::event::EnableMouseCapture;
9
use crossterm::terminal::disable_raw_mode;
10
use crossterm::terminal::enable_raw_mode;
11
use crossterm::terminal::is_raw_mode_enabled;
12
use crossterm::terminal::EnterAlternateScreen;
13
use crossterm::terminal::LeaveAlternateScreen;
14
use ratatui::backend::CrosstermBackend;
15
use ratatui::prelude::*;
16
use ratatui::widgets::Paragraph;
17
use ratatui::widgets::Scrollbar;
18
use ratatui::widgets::ScrollbarOrientation;
19
use ratatui::Terminal;
20
use tokio::sync::mpsc;
21

22
use crate::config::Config;
23
use crate::config::ConfigKey;
24
use crate::domain::models::Action;
25
use crate::domain::models::Author;
26
use crate::domain::models::BackendName;
27
use crate::domain::models::BackendPrompt;
28
use crate::domain::models::EditorName;
29
use crate::domain::models::Event;
30
use crate::domain::models::Loading;
31
use crate::domain::models::Message;
32
use crate::domain::models::SlashCommand;
33
use crate::domain::models::TextArea;
34
use crate::domain::services::events::EventsService;
35
use crate::domain::services::AppState;
36
use crate::domain::services::AppStateProps;
37
use crate::domain::services::Bubble;
38
use crate::infrastructure::backends::BackendManager;
39
use crate::infrastructure::editors::EditorManager;
40

41
/// Verifies that the current window size is large enough to handle the bare
42
/// minimum width that includes the model name, username, bubbles, and padding.
43
fn is_line_width_sufficient(line_width: u16) -> bool {
×
44
    let author_lengths = vec![Author::User, Author::Oatmeal, Author::Model]
×
45
        .into_iter()
×
46
        .map(|e| return e.to_string().len())
×
47
        .max()
×
48
        .unwrap();
×
49

×
50
    let bubble_style = Bubble::style_confg();
×
51
    let min_width =
×
52
        (author_lengths + bubble_style.bubble_padding + bubble_style.border_elements_length) as i32;
×
53
    let trimmed_line_width =
×
54
        ((line_width as f32 * (1.0 - bubble_style.outer_padding_percentage)).ceil()) as i32;
×
55

×
56
    return trimmed_line_width >= min_width;
×
57
}
×
58

59
async fn start_loop<B: Backend>(
×
60
    terminal: &mut Terminal<B>,
×
61
    app_state_props: AppStateProps,
×
62
    tx: mpsc::UnboundedSender<Action>,
×
63
    rx: mpsc::UnboundedReceiver<Event>,
×
64
) -> Result<()> {
×
65
    let mut events = EventsService::new(rx);
×
66
    let mut textarea = TextArea::default();
×
67
    let mut app_state = AppState::new(app_state_props).await?;
×
68
    let loading = Loading::default();
×
69

70
    #[cfg(feature = "dev")]
71
    {
72
        let test_str = "Write a function in Java that prints from 0 to 10. Describe the example before and after.";
73
        textarea.insert_str(test_str);
74
    }
75

76
    loop {
77
        terminal.draw(|frame| {
×
78
            if !is_line_width_sufficient(frame.size().width) {
×
79
                frame.render_widget(
×
80
                    Paragraph::new("I'm too small, make me bigger!").alignment(Alignment::Left),
×
81
                    frame.size(),
×
82
                );
×
83
                return;
×
84
            }
×
85

×
86
            let textarea_len = (textarea.lines().len() + 3).try_into().unwrap();
×
87
            let layout = Layout::default()
×
88
                .direction(Direction::Vertical)
×
89
                .constraints(vec![Constraint::Min(1), Constraint::Max(textarea_len)])
×
90
                .split(frame.size());
×
91

×
92
            if layout[0].width as usize != app_state.last_known_width
×
93
                || layout[0].height as usize != app_state.last_known_height
×
94
            {
×
95
                app_state.set_rect(layout[0]);
×
96
            }
×
97

98
            app_state.bubble_list.render(
×
99
                layout[0],
×
100
                frame.buffer_mut(),
×
101
                app_state.scroll.position.try_into().unwrap(),
×
102
            );
×
103

×
104
            frame.render_stateful_widget(
×
105
                Scrollbar::new(ScrollbarOrientation::VerticalRight),
×
106
                layout[0].inner(&Margin {
×
107
                    vertical: 1,
×
108
                    horizontal: 0,
×
109
                }),
×
110
                &mut app_state.scroll.scrollbar_state,
×
111
            );
×
112

×
113
            if app_state.waiting_for_backend {
×
114
                loading.render(frame, layout[1]);
×
115
            } else {
×
116
                frame.render_widget(textarea.widget(), layout[1]);
×
117
            }
×
118
        })?;
×
119

120
        macro_rules! send_user_message {
121
            ( $input_str:expr ) => {
122
                let input_str = $input_str;
123

124
                let msg = Message::new(Author::User, &input_str);
125
                textarea = TextArea::default();
126
                app_state.add_message(msg);
127

128
                let (should_break, should_continue) =
129
                    app_state.handle_slash_commands(input_str, &tx)?;
130

131
                if should_break {
132
                    break;
133
                }
134
                if should_continue {
135
                    continue;
136
                }
137

138
                app_state.waiting_for_backend = true;
139
                let mut prompt =
140
                    BackendPrompt::new(input_str.to_string(), app_state.backend_context.clone());
141

142
                if app_state.backend_context.is_empty() && SlashCommand::parse(&input_str).is_none()
143
                {
144
                    prompt.append_chat_context(&app_state.editor_context);
145
                }
146

147
                tx.send(Action::BackendRequest(prompt))?;
148
                app_state.save_session().await?;
149
            };
150
        }
151

152
        match events.next().await? {
×
153
            Event::BackendMessage(msg) => {
×
154
                app_state.add_message(msg);
×
155
                app_state.waiting_for_backend = false;
×
156
            }
×
157
            Event::BackendPromptResponse(msg) => {
×
158
                app_state.handle_backend_response(msg.clone());
×
159
                if msg.done {
×
160
                    app_state.save_session().await?;
×
161
                }
×
162
            }
163
            Event::KeyboardCharInput(input) => {
×
164
                if app_state.waiting_for_backend {
×
165
                    continue;
×
166
                }
×
167

×
168
                // Windows submits a null event right after CTRL+C. Ignore it.
×
169
                if input.key != tui_textarea::Key::Null {
×
170
                    app_state.exit_warning = false;
×
171
                }
×
172

173
                textarea.input(input);
×
174
            }
175
            Event::KeyboardCTRLC() => {
176
                if app_state.waiting_for_backend {
×
177
                    app_state.waiting_for_backend = false;
×
178
                    tx.send(Action::BackendAbort())?;
×
179
                } else if !app_state.exit_warning {
×
180
                    app_state.add_message(Message::new(
×
181
                        Author::Oatmeal,
×
182
                        "If you wish to quit, hit CTRL+C one more time, or use /quit",
×
183
                    ));
×
184
                    app_state.exit_warning = true;
×
185
                } else {
×
186
                    break;
×
187
                }
188
            }
189
            Event::KeyboardCTRLR() => {
190
                let last_message = app_state
×
191
                    .messages
×
192
                    .iter()
×
193
                    .filter(|msg| {
×
194
                        return msg.author == Author::User
×
195
                            && SlashCommand::parse(&msg.text).is_none();
×
196
                    })
×
197
                    .last();
×
198
                if let Some(message) = last_message.cloned() {
×
199
                    send_user_message!(&message.text);
×
200
                }
×
201
            }
202
            Event::KeyboardEnter() => {
203
                if app_state.waiting_for_backend {
×
204
                    continue;
×
205
                }
×
206
                let input_str = &textarea.lines().join("\n");
×
207
                if input_str.is_empty() {
×
208
                    continue;
×
209
                }
×
210
                send_user_message!(input_str);
×
211
            }
212
            Event::KeyboardPaste(text) => {
×
213
                if app_state.waiting_for_backend {
×
214
                    continue;
×
215
                }
×
216
                app_state.exit_warning = false;
×
217
                textarea.set_yank_text(text.replace('\r', "\n"));
×
218
                textarea.paste();
×
219
            }
220
            Event::UITick() => {
221
                continue;
×
222
            }
223
            Event::UIScrollDown() => {
×
224
                app_state.scroll.down();
×
225
            }
×
226
            Event::UIScrollUp() => {
×
227
                app_state.scroll.up();
×
228
            }
×
229
            Event::UIScrollPageDown() => {
×
230
                app_state.scroll.down_page();
×
231
            }
×
232
            Event::UIScrollPageUp() => {
×
233
                app_state.scroll.up_page();
×
234
            }
×
235
        }
236
    }
237

238
    return Ok(());
×
239
}
×
240

241
pub fn destruct_terminal_for_panic() {
242
    if let Ok(enabled) = is_raw_mode_enabled() {
×
243
        if enabled {
×
244
            let _ = disable_raw_mode();
×
245
            let _ = crossterm::execute!(
×
246
                io::stdout(),
×
247
                LeaveAlternateScreen,
×
248
                DisableMouseCapture,
×
249
                DisableBracketedPaste
×
250
            );
×
251
            let _ = crossterm::execute!(io::stdout(), cursor::Show);
×
252
        }
×
253
    }
×
254
}
×
255

256
pub async fn start(
×
257
    tx: mpsc::UnboundedSender<Action>,
×
258
    rx: mpsc::UnboundedReceiver<Event>,
×
259
) -> Result<()> {
×
260
    let stdout = io::stdout();
×
261
    let mut stdout = stdout.lock();
×
262

×
263
    enable_raw_mode()?;
×
264
    crossterm::execute!(
×
265
        stdout,
×
266
        EnterAlternateScreen,
×
267
        EnableMouseCapture,
×
268
        EnableBracketedPaste
×
269
    )?;
×
270
    let term_backend = CrosstermBackend::new(stdout);
×
271
    let mut terminal = Terminal::new(term_backend)?;
×
272
    let editor_name = EditorName::parse(Config::get(ConfigKey::Editor)).unwrap();
×
273
    let mut session_id = None;
×
274
    if !Config::get(ConfigKey::SessionID).is_empty() {
×
275
        session_id = Some(Config::get(ConfigKey::SessionID));
×
276
    }
×
277

NEW
278
    let backend =
×
NEW
279
        BackendManager::get(BackendName::parse(Config::get(ConfigKey::Backend)).unwrap())?;
×
NEW
280
    let editor = EditorManager::get(EditorName::parse(Config::get(ConfigKey::Editor)).unwrap())?;
×
281
    let app_state_pros = AppStateProps {
×
NEW
282
        backend,
×
NEW
283
        editor,
×
284
        model_name: Config::get(ConfigKey::Model),
×
285
        theme_name: Config::get(ConfigKey::Theme),
×
286
        theme_file: Config::get(ConfigKey::ThemeFile),
×
287
        session_id,
×
288
    };
×
289

×
290
    start_loop(&mut terminal, app_state_pros, tx, rx).await?;
×
291
    let editor = EditorManager::get(editor_name)?;
×
292
    if editor.health_check().await.is_ok() {
×
293
        editor.clear_context().await?;
×
294
    }
×
295

296
    disable_raw_mode()?;
×
297
    crossterm::execute!(
×
298
        terminal.backend_mut(),
×
299
        LeaveAlternateScreen,
×
300
        DisableMouseCapture,
×
301
        DisableBracketedPaste
×
302
    )?;
×
303
    terminal.show_cursor()?;
×
304

305
    return Ok(());
×
306
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc