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

dustinblackman / oatmeal / 7280395059

20 Dec 2023 08:21PM UTC coverage: 44.84% (-0.2%) from 45.041%
7280395059

push

github

dustinblackman
feat!: Default to first model in backend if none selected at start

2 of 27 new or added lines in 3 files covered. (7.41%)

2 existing lines in 1 file now uncovered.

1108 of 2471 relevant lines covered (44.84%)

17.8 hits per line

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

0.0
/src/application/cli.rs
1
use std::env;
2
use std::io;
3

4
use anyhow::Result;
5
use clap::builder::PossibleValuesParser;
6
use clap::value_parser;
7
use clap::Arg;
8
use clap::ArgAction;
9
use clap::ArgGroup;
10
use clap::Command;
11
use clap_complete::generate;
12
use clap_complete::Generator;
13
use clap_complete::Shell;
14
use dialoguer::theme::ColorfulTheme;
15
use dialoguer::Select;
16
use strum::VariantNames;
17
use yansi::Paint;
18

19
use crate::config::Config;
20
use crate::config::ConfigKey;
21
use crate::domain::models::BackendName;
22
use crate::domain::models::EditorName;
23
use crate::domain::models::Session;
24
use crate::domain::services::actions::help_text;
25
use crate::domain::services::Sessions;
26
use crate::domain::services::Syntaxes;
27
use crate::domain::services::Themes;
28

29
fn print_completions<G: Generator>(gen: G, cmd: &mut Command) {
×
30
    generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout());
×
31
    std::process::exit(0);
×
32
}
33

34
fn format_session(session: &Session) -> String {
×
35
    let mut res = format!(
×
36
        "- (ID: {}) {}, Model: {}",
×
37
        session.id, session.timestamp, session.state.backend_model,
×
38
    );
×
39

×
40
    if !session.state.editor_language.is_empty() {
×
41
        res = format!("{res}, Lang: {}", session.state.editor_language)
×
42
    }
×
43

44
    if !session.state.messages.is_empty() {
×
45
        let mut line = session.state.messages[0]
×
46
            .text
×
47
            .split('\n')
×
48
            .collect::<Vec<_>>()[0]
×
49
            .to_string();
×
50

×
51
        if line.len() >= 70 {
×
52
            line = format!("{}...", &line[..67]);
×
53
        }
×
54
        res = format!("{res}, {line}");
×
55
    }
×
56

57
    return res;
×
58
}
×
59

60
async fn print_sessions_list() -> Result<()> {
×
61
    let mut sessions = Sessions::default()
×
62
        .list()
×
63
        .await?
×
64
        .iter()
×
65
        .map(|session| {
×
66
            return format_session(session);
×
67
        })
×
68
        .collect::<Vec<String>>();
×
69

×
70
    sessions.reverse();
×
71

×
72
    if sessions.is_empty() {
×
73
        println!("There are no sessions available. You should start your first one!");
×
74
    } else {
×
75
        println!("{}", sessions.join("\n"));
×
76
    }
×
77

78
    return Ok(());
×
79
}
×
80

81
async fn load_config_from_session(session_id: &str) -> Result<()> {
×
82
    let session = Sessions::default().load(session_id).await?;
×
83
    Config::set(ConfigKey::Backend, &session.state.backend_name);
×
84
    Config::set(ConfigKey::Model, &session.state.backend_model);
×
85
    Config::set(ConfigKey::SessionID, session_id);
×
86

×
87
    return Ok(());
×
88
}
×
89

90
async fn load_config_from_session_interactive() -> Result<()> {
×
91
    let mut sessions = Sessions::default().list().await?;
×
92
    sessions.reverse();
×
93

×
94
    if sessions.is_empty() {
×
95
        println!("There are no sessions available. You should start your first one!");
×
96
        return Ok(());
×
97
    }
×
98

×
99
    let session_options = sessions
×
100
        .iter()
×
101
        .map(|session| {
×
102
            return format_session(session);
×
103
        })
×
104
        .collect::<Vec<String>>();
×
105

106
    let idx = Select::with_theme(&ColorfulTheme::default())
×
107
        .with_prompt("Which session would you like to load?")
×
108
        .default(0)
×
109
        .items(&session_options)
×
110
        .interact_opt()?
×
111
        .unwrap();
×
112

×
113
    load_config_from_session(&sessions[idx].id).await?;
×
114

115
    return Ok(());
×
116
}
×
117

118
fn subcommand_debug() -> Command {
×
119
    return Command::new("debug")
×
120
        .about("Debug helpers for Oatmeal")
×
121
        .hide(true)
×
122
        .subcommand(
×
123
            Command::new("syntaxes").about("List all supported code highlighting languages.")
×
124
        )
×
125
        .subcommand(
×
126
            Command::new("themes").about("List all supported code highlighting themes.")
×
127
        )
×
128
        .subcommand(
×
129
            Command::new("log-path").about("Output path to debug log file generated when running Oatmeal with environment variable RUST_LOG=oatmeal")
×
130
        )
×
131
        .subcommand(
×
132
            Command::new("enum-config").about("List all config keys as strings.")
×
133
        );
×
134
}
×
135

136
fn subcommand_completions() -> Command {
×
137
    return Command::new("completions")
×
138
        .about("Generates shell completions")
×
139
        .arg(
×
140
            clap::Arg::new("shell")
×
141
                .short('s')
×
142
                .long("shell")
×
143
                .help("Which shell to generate completions for.")
×
144
                .action(ArgAction::Set)
×
145
                .value_parser(value_parser!(Shell))
×
146
                .required(true),
×
147
        );
×
148
}
×
149

150
fn subcommand_sessions_delete() -> Command {
×
151
    return Command::new("delete")
×
NEW
152
        .about("Delete one or all sessions.")
×
153
        .arg(
×
154
            clap::Arg::new("session-id")
×
155
                .short('i')
×
156
                .long("id")
×
157
                .help("Session ID")
×
158
                .num_args(1),
×
159
        )
×
160
        .arg(
×
161
            clap::Arg::new("all")
×
162
                .long("all")
×
NEW
163
                .help("Delete all sessions.")
×
164
                .num_args(0),
×
165
        )
×
166
        .group(
×
167
            ArgGroup::new("delete-args")
×
168
                .args(["session-id", "all"])
×
169
                .required(true),
×
170
        );
×
171
}
×
172

173
fn arg_backend() -> Arg {
×
174
    return Arg::new("backend")
×
175
        .short('b')
×
176
        .long("backend")
×
177
        .env("OATMEAL_BACKEND")
×
178
        .num_args(1)
×
179
        .help("The initial backend hosting a model to connect to.")
×
180
        .value_parser(PossibleValuesParser::new(BackendName::VARIANTS))
×
181
        .default_value("ollama");
×
182
}
×
183

184
fn arg_backend_health_check_timeout() -> Arg {
×
185
    return Arg::new("backend-health-check-timeout")
×
186
        .long("backend-health-check-timeout")
×
187
        .env("OATMEAL_BACKEND_HEALTH_CHECK_TIMEOUT")
×
188
        .num_args(1)
×
189
        .help(
×
NEW
190
            "Time to wait in milliseconds before timing out when doing a healthcheck for a backend.",
×
191
        )
×
192
        .default_value("1000");
×
193
}
×
194

195
fn arg_model() -> Arg {
×
196
    return Arg::new("model")
×
197
        .short('m')
×
198
        .long("model")
×
199
        .env("OATMEAL_MODEL")
×
200
        .num_args(1)
×
NEW
201
        .help("The initial model on a backend to consume. Defaults to the first model available from the backend if not set.");
×
202
}
×
203

204
fn subcommand_chat() -> Command {
×
205
    return Command::new("chat")
×
206
        .about("Start a new chat session")
×
207
        .arg(arg_backend())
×
208
        .arg(arg_backend_health_check_timeout())
×
209
        .arg(arg_model());
×
210
}
×
211

212
fn subcommand_sessions() -> Command {
×
213
    return Command::new("sessions")
×
214
        .about("Manage past chat sessions")
×
215
        .arg_required_else_help(true)
×
216
        .subcommand(Command::new("dir").about("Print the sessions cache directory path."))
×
217
        .subcommand(Command::new("list").about("List all previous sessions with their ids and models."))
×
218
        .subcommand(
×
219
            Command::new("open")
×
220
                .about("Open a previous session by ID. Omit passing any session ID to load an interactive selection.")
×
221
                .arg(
×
222
                    clap::Arg::new("session-id")
×
223
                        .short('i')
×
224
                        .long("id")
×
225
                        .help("Session ID")
×
226
                        .required(false),
×
227
                ),
×
228
        )
×
229
        .subcommand(subcommand_sessions_delete());
×
230
}
×
231

232
fn build() -> Command {
×
233
    let commands_text = help_text()
×
234
        .split('\n')
×
235
        .map(|line| {
×
236
            if line.starts_with('-') {
×
237
                return format!("  {line}");
×
238
            }
×
239
            if line.starts_with("COMMANDS:")
×
240
                || line.starts_with("HOTKEYS:")
×
241
                || line.starts_with("CODE ACTIONS:")
×
242
            {
243
                return Paint::new(format!("CHAT {line}"))
×
244
                    .underline()
×
245
                    .bold()
×
246
                    .to_string();
×
247
            }
×
248
            return line.to_string();
×
249
        })
×
250
        .collect::<Vec<String>>()
×
251
        .join("\n");
×
252

×
253
    let about = format!(
×
254
        "{}\n\nVersion: {}\nCommit: {}",
×
255
        env!("CARGO_PKG_DESCRIPTION"),
×
256
        env!("CARGO_PKG_VERSION"),
×
257
        env!("VERGEN_GIT_DESCRIBE")
×
258
    );
×
259

×
260
    let themes = Themes::list();
×
261

×
262
    return Command::new("oatmeal")
×
263
        .about(about)
×
264
        .author(env!("CARGO_PKG_AUTHORS"))
×
265
        .version(env!("CARGO_PKG_VERSION"))
×
266
        .after_help(commands_text)
×
267
        .arg_required_else_help(false)
×
268
        .subcommand(subcommand_chat())
×
269
        .subcommand(subcommand_completions())
×
270
        .subcommand(subcommand_debug())
×
271
        .subcommand(subcommand_sessions())
×
272
        .arg(arg_backend())
×
273
        .arg(arg_backend_health_check_timeout())
×
274
        .arg(arg_model())
×
275
        .arg(
×
276
            Arg::new("editor")
×
277
                .short('e')
×
278
                .long("editor")
×
279
                .env("OATMEAL_EDITOR")
×
280
                .num_args(1)
×
281
                .help("The editor to integrate with.")
×
282
                .value_parser(PossibleValuesParser::new(EditorName::VARIANTS))
×
283
                .default_value("clipboard")
×
284
                .global(true),
×
285
        )
×
286
        .arg(
×
287
            Arg::new("theme")
×
288
                .short('t')
×
289
                .long("theme")
×
290
                .env("OATMEAL_THEME")
×
291
                .num_args(1)
×
292
                .help("Sets code syntax highlighting theme.")
×
293
                .value_parser(PossibleValuesParser::new(themes))
×
294
                .default_value("base16-onedark")
×
295
                .global(true),
×
296
        )
×
297
        .arg(
×
298
            Arg::new("theme-file")
×
299
                .long("theme-file")
×
300
                .env("OATMEAL_THEME_FILE")
×
301
                .num_args(1)
×
302
                .help(
×
303
                    "Absolute path to a TextMate tmTheme to use for code syntax highlighting."
×
304
                )
×
305
                .global(true),
×
306
        )
×
307
        .arg(
×
308
            Arg::new("ollama-url")
×
309
                .long("ollama-url")
×
310
                .env("OATMEAL_OLLAMA_URL")
×
311
                .num_args(1)
×
312
                .help("Ollama API URL when using the Ollama backend.")
×
313
                .default_value("http://localhost:11434")
×
314
                .global(true),
×
315
        )
×
316
        .arg(
×
317
            Arg::new("openai-url")
×
318
                .long("openai-url")
×
319
                .env("OATMEAL_OPENAI_URL")
×
320
                .num_args(1)
×
321
                .help("OpenAI API URL when using the OpenAI backend. Can be swapped to a compatible proxy.")
×
322
                .default_value("https://api.openai.com")
×
323
                .global(true),
×
324
        )
×
325
        .arg(
×
326
            Arg::new("openai-token")
×
327
                .long("openai-token")
×
328
                .env("OATMEAL_OPENAI_TOKEN")
×
329
                .num_args(1)
×
330
                .help("OpenAI API token when using the OpenAI backend.")
×
331
                .global(true),
×
332
        );
×
333
}
×
334

335
pub async fn parse() -> Result<bool> {
×
336
    let matches = build().get_matches();
×
337

×
338
    match matches.subcommand() {
×
339
        Some(("debug", debug_matches)) => {
×
340
            match debug_matches.subcommand() {
×
341
                Some(("syntaxes", _)) => {
×
342
                    println!("{}", Syntaxes::list().join("\n"));
×
343
                    return Ok(false);
×
344
                }
345
                Some(("themes", _)) => {
×
346
                    println!("{}", Themes::list().join("\n"));
×
347
                    return Ok(false);
×
348
                }
349
                Some(("log-path", _)) => {
×
350
                    let log_path = dirs::cache_dir().unwrap().join("oatmeal/debug.log");
×
351
                    println!("{}", log_path.to_str().unwrap());
×
352
                    return Ok(false);
×
353
                }
354
                Some(("enum-config", _)) => {
×
355
                    let res = ConfigKey::VARIANTS.join("\n");
×
356
                    println!("{}", res);
×
357
                    return Ok(false);
×
358
                }
359
                _ => {
360
                    subcommand_debug().print_long_help()?;
×
361
                    return Ok(false);
×
362
                }
363
            }
364
        }
365
        Some(("chat", subcmd_matches)) => {
×
366
            Config::set(
×
367
                ConfigKey::Backend,
×
368
                subcmd_matches.get_one::<String>("backend").unwrap(),
×
369
            );
×
370
            Config::set(
×
371
                ConfigKey::BackendHealthCheckTimeout,
×
372
                matches
×
373
                    .get_one::<String>("backend-health-check-timeout")
×
374
                    .unwrap(),
×
375
            );
×
376
            Config::set(
×
377
                ConfigKey::Model,
×
378
                subcmd_matches.get_one::<String>("model").unwrap(),
×
379
            );
×
380
        }
×
381
        Some(("completions", subcmd_matches)) => {
×
382
            if let Some(completions) = subcmd_matches.get_one::<Shell>("shell").copied() {
×
383
                let mut app = build();
×
384
                print_completions(completions, &mut app);
×
385
            }
×
386
        }
387
        Some(("sessions", subcmd_matches)) => {
×
388
            match subcmd_matches.subcommand() {
×
389
                Some(("dir", _)) => {
×
390
                    let dir = Sessions::default().cache_dir.to_string_lossy().to_string();
×
391
                    println!("{dir}");
×
392
                    return Ok(false);
×
393
                }
394
                Some(("list", _)) => {
×
395
                    print_sessions_list().await?;
×
396
                    return Ok(false);
×
397
                }
398
                Some(("open", open_matches)) => {
×
399
                    if let Some(session_id) = open_matches.get_one::<String>("session-id") {
×
400
                        load_config_from_session(session_id).await?;
×
401
                    } else {
402
                        load_config_from_session_interactive().await?;
×
403
                    }
404
                }
405
                Some(("delete", delete_matches)) => {
×
406
                    if let Some(session_id) = delete_matches.get_one::<String>("session-id") {
×
407
                        Sessions::default().delete(session_id).await?;
×
408
                        println!("Deleted session {session_id}");
×
409
                    } else if delete_matches.get_one::<bool>("all").is_some() {
×
410
                        Sessions::default().delete_all().await?;
×
411
                        println!("Deleted all sessions");
×
412
                    } else {
413
                        subcommand_sessions_delete().print_long_help()?;
×
414
                    }
415
                    return Ok(false);
×
416
                }
417
                _ => {
418
                    subcommand_sessions().print_long_help()?;
×
419
                    return Ok(false);
×
420
                }
421
            }
422
        }
423
        _ => {
424
            Config::set(
×
425
                ConfigKey::Backend,
×
426
                matches.get_one::<String>("backend").unwrap(),
×
427
            );
×
428
            Config::set(
×
429
                ConfigKey::BackendHealthCheckTimeout,
×
430
                matches
×
431
                    .get_one::<String>("backend-health-check-timeout")
×
432
                    .unwrap(),
×
433
            );
×
NEW
434
            if let Some(model) = matches.get_one::<String>("model") {
×
NEW
435
                Config::set(ConfigKey::Model, model);
×
NEW
436
            }
×
437
        }
438
    }
439

440
    Config::set(
×
441
        ConfigKey::Editor,
×
442
        matches.get_one::<String>("editor").unwrap(),
×
443
    );
×
444
    Config::set(
×
445
        ConfigKey::Theme,
×
446
        matches.get_one::<String>("theme").unwrap(),
×
447
    );
×
448
    Config::set(
×
449
        ConfigKey::OllamaURL,
×
450
        matches.get_one::<String>("ollama-url").unwrap(),
×
451
    );
×
452
    Config::set(
×
453
        ConfigKey::OpenAiURL,
×
454
        matches.get_one::<String>("openai-url").unwrap(),
×
455
    );
×
456

×
457
    let mut user = env::var("USER").unwrap_or_else(|_| return "".to_string());
×
458
    if user.is_empty() {
×
459
        user = "User".to_string();
×
460
    }
×
461
    Config::set(ConfigKey::Username, &user);
×
462

463
    if let Some(theme_file) = matches.get_one::<String>("theme-file") {
×
464
        Config::set(ConfigKey::ThemeFile, theme_file);
×
465
    }
×
466

467
    if let Some(openai_token) = matches.get_one::<String>("openai-token") {
×
468
        Config::set(ConfigKey::OpenAiToken, openai_token);
×
469
    }
×
470

471
    tracing::debug!(
×
472
        username = Config::get(ConfigKey::Username),
×
473
        backend = Config::get(ConfigKey::Backend),
×
474
        editor = Config::get(ConfigKey::Editor),
×
475
        model = Config::get(ConfigKey::Model),
×
476
        theme = Config::get(ConfigKey::Theme),
×
477
        theme_file = Config::get(ConfigKey::ThemeFile),
×
478
        "config"
×
479
    );
×
480

481
    return Ok(true);
×
482
}
×
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