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

dustinblackman / oatmeal / 7305824964

23 Dec 2023 03:05AM UTC coverage: 59.165% (+12.8%) from 46.412%
7305824964

push

github

dustinblackman
fix: Lint

1 of 1 new or added line in 1 file covered. (100.0%)

22 existing lines in 2 files now uncovered.

1630 of 2755 relevant lines covered (59.17%)

17.81 hits per line

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

58.6
/src/application/cli.rs
1
use std::env;
2
use std::io;
3
use std::path;
4

5
use anyhow::bail;
6
use anyhow::Result;
7
use clap::builder::PossibleValuesParser;
8
use clap::value_parser;
9
use clap::Arg;
10
use clap::ArgAction;
11
use clap::ArgGroup;
12
use clap::Command;
13
use clap_complete::generate;
14
use clap_complete::Generator;
15
use clap_complete::Shell;
16
use dialoguer::theme::ColorfulTheme;
17
use dialoguer::Select;
18
use strum::VariantNames;
19
use tokio::fs;
20
use tokio::io::AsyncWriteExt;
21
use yansi::Paint;
22

23
use crate::configuration::Config;
24
use crate::configuration::ConfigKey;
25
use crate::domain::models::BackendName;
26
use crate::domain::models::EditorName;
27
use crate::domain::models::Session;
28
use crate::domain::services::actions::help_text;
29
use crate::domain::services::Sessions;
30
use crate::domain::services::Syntaxes;
31
use crate::domain::services::Themes;
32

UNCOV
33
fn print_completions<G: Generator>(gen: G, cmd: &mut Command) {
×
34
    generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout());
×
35
    std::process::exit(0);
×
36
}
37

38
fn format_session(session: &Session) -> String {
×
39
    let mut res = format!(
×
40
        "- (ID: {}) {}, Model: {}",
×
41
        session.id, session.timestamp, session.state.backend_model,
×
42
    );
×
UNCOV
43

×
44
    if !session.state.editor_language.is_empty() {
×
45
        res = format!("{res}, Lang: {}", session.state.editor_language)
×
46
    }
×
47

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

×
55
        if line.len() >= 70 {
×
UNCOV
56
            line = format!("{}...", &line[..67]);
×
57
        }
×
58
        res = format!("{res}, {line}");
×
UNCOV
59
    }
×
60

61
    return res;
×
62
}
×
63

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

×
74
    sessions.reverse();
×
75

×
76
    if sessions.is_empty() {
×
UNCOV
77
        println!("There are no sessions available. You should start your first one!");
×
78
    } else {
×
79
        println!("{}", sessions.join("\n"));
×
UNCOV
80
    }
×
81

82
    return Ok(());
×
83
}
×
84

85
async fn create_config_file() -> Result<()> {
×
86
    let config_file_path_str = Config::default(ConfigKey::ConfigFile);
×
87
    let config_file_path = path::PathBuf::from(&config_file_path_str);
×
88
    if config_file_path.exists() {
×
UNCOV
89
        bail!(format!(
×
90
            "Config file already exists at {config_file_path_str}"
×
91
        ));
×
92
    }
×
93

×
94
    if !config_file_path.parent().unwrap().exists() {
×
95
        fs::create_dir_all(config_file_path.parent().unwrap()).await?;
×
96
    }
×
97

98
    let mut file = fs::File::create(config_file_path).await?;
×
99
    file.write_all(Config::serialize_default(build()).as_bytes())
×
100
        .await?;
×
101

102
    println!("Created default config file at {config_file_path_str}");
×
103
    return Ok(());
×
104
}
×
105

106
async fn load_config_from_session(session_id: &str) -> Result<()> {
×
107
    let session = Sessions::default().load(session_id).await?;
×
108
    Config::set(ConfigKey::Backend, &session.state.backend_name);
×
109
    Config::set(ConfigKey::Model, &session.state.backend_model);
×
110
    Config::set(ConfigKey::SessionID, session_id);
×
111

×
112
    return Ok(());
×
113
}
×
114

115
async fn load_config_from_session_interactive() -> Result<()> {
×
116
    let mut sessions = Sessions::default().list().await?;
×
UNCOV
117
    sessions.reverse();
×
118

×
119
    if sessions.is_empty() {
×
120
        println!("There are no sessions available. You should start your first one!");
×
121
        return Ok(());
×
122
    }
×
123

×
124
    let session_options = sessions
×
125
        .iter()
×
126
        .map(|session| {
×
127
            return format_session(session);
×
128
        })
×
129
        .collect::<Vec<String>>();
×
130

131
    let idx = Select::with_theme(&ColorfulTheme::default())
×
132
        .with_prompt("Which session would you like to load?")
×
133
        .default(0)
×
134
        .items(&session_options)
×
UNCOV
135
        .interact_opt()?
×
136
        .unwrap();
×
137

×
138
    load_config_from_session(&sessions[idx].id).await?;
×
139

140
    return Ok(());
×
141
}
×
142

143
fn subcommand_completions() -> Command {
5✔
144
    return Command::new("completions")
5✔
145
        .about("Generates shell completions.")
5✔
146
        .arg(
5✔
147
            clap::Arg::new("shell")
5✔
148
                .short('s')
5✔
149
                .long("shell")
5✔
150
                .help("Which shell to generate completions for.")
5✔
151
                .action(ArgAction::Set)
5✔
152
                .value_parser(value_parser!(Shell))
5✔
153
                .required(true),
5✔
154
        );
5✔
155
}
5✔
156

157
fn subcommand_config() -> Command {
5✔
158
    return Command::new("config")
5✔
159
        .about("Configuration file options.")
5✔
160
        .subcommand(
5✔
161
            Command::new("create").about("Saves the default config file to the configuration file path. This command will fail if the file exists already.")
5✔
162
        )
5✔
163
        .subcommand(
5✔
164
            Command::new("default").about("Outputs the default configuration file to stdout.")
5✔
165
        )
5✔
166
        .subcommand(
5✔
167
            Command::new("path").about("Returns the default path for the configuration file.")
5✔
168
        );
5✔
169
}
5✔
170

171
fn subcommand_debug() -> Command {
5✔
172
    return Command::new("debug")
5✔
173
        .about("Debug helpers for Oatmeal")
5✔
174
        .hide(true)
5✔
175
        .subcommand(
5✔
176
            Command::new("syntaxes").about("List all supported code highlighting languages.")
5✔
177
        )
5✔
178
        .subcommand(
5✔
179
            Command::new("themes").about("List all supported code highlighting themes.")
5✔
180
        )
5✔
181
        .subcommand(
5✔
182
            Command::new("log-path").about("Output path to debug log file generated when running Oatmeal with environment variable RUST_LOG=oatmeal")
5✔
183
        )
5✔
184
        .subcommand(
5✔
185
            Command::new("enum-config").about("List all config keys as strings.")
5✔
186
        );
5✔
187
}
5✔
188

189
fn subcommand_sessions_delete() -> Command {
5✔
190
    return Command::new("delete")
5✔
191
        .about("Delete one or all sessions.")
5✔
192
        .arg(
5✔
193
            clap::Arg::new("session-id")
5✔
194
                .short('i')
5✔
195
                .long("id")
5✔
196
                .help("Session ID")
5✔
197
                .num_args(1),
5✔
198
        )
5✔
199
        .arg(
5✔
200
            clap::Arg::new("all")
5✔
201
                .long("all")
5✔
202
                .help("Delete all sessions.")
5✔
203
                .num_args(0),
5✔
204
        )
5✔
205
        .group(
5✔
206
            ArgGroup::new("delete-args")
5✔
207
                .args(["session-id", "all"])
5✔
208
                .required(true),
5✔
209
        );
5✔
210
}
5✔
211

212
fn arg_backend() -> Arg {
10✔
213
    return Arg::new(ConfigKey::Backend.to_string())
10✔
214
        .short('b')
10✔
215
        .long(ConfigKey::Backend.to_string())
10✔
216
        .env("OATMEAL_BACKEND")
10✔
217
        .num_args(1)
10✔
218
        .help(format!(
10✔
219
            "The initial backend hosting a model to connect to. [default: {}]",
10✔
220
            Config::default(ConfigKey::Backend)
10✔
221
        ))
10✔
222
        .value_parser(PossibleValuesParser::new(BackendName::VARIANTS));
10✔
223
}
10✔
224

225
fn arg_backend_health_check_timeout() -> Arg {
10✔
226
    return Arg::new(ConfigKey::BackendHealthCheckTimeout.to_string())
10✔
227
        .long(ConfigKey::BackendHealthCheckTimeout.to_string())
10✔
228
        .env("OATMEAL_BACKEND_HEALTH_CHECK_TIMEOUT")
10✔
229
        .num_args(1)
10✔
230
        .help(
10✔
231
            format!("Time to wait in milliseconds before timing out when doing a healthcheck for a backend. [default: {}]", Config::default(ConfigKey::BackendHealthCheckTimeout)),
10✔
232
        );
10✔
233
}
10✔
234

235
fn arg_model() -> Arg {
10✔
236
    return Arg::new(ConfigKey::Model.to_string())
10✔
237
        .short('m')
10✔
238
        .long(ConfigKey::Model.to_string())
10✔
239
        .env("OATMEAL_MODEL")
10✔
240
        .num_args(1)
10✔
241
        .help("The initial model on a backend to consume. Defaults to the first model available from the backend if not set.");
10✔
242
}
10✔
243

244
fn subcommand_chat() -> Command {
5✔
245
    return Command::new("chat")
5✔
246
        .about("Start a new chat session.")
5✔
247
        .arg(arg_backend())
5✔
248
        .arg(arg_backend_health_check_timeout())
5✔
249
        .arg(arg_model());
5✔
250
}
5✔
251

252
fn subcommand_sessions() -> Command {
5✔
253
    return Command::new("sessions")
5✔
254
        .about("Manage past chat sessions.")
5✔
255
        .arg_required_else_help(true)
5✔
256
        .subcommand(Command::new("dir").about("Print the sessions cache directory path."))
5✔
257
        .subcommand(Command::new("list").about("List all previous sessions with their ids and models."))
5✔
258
        .subcommand(
5✔
259
            Command::new("open")
5✔
260
                .about("Open a previous session by ID. Omit passing any session ID to load an interactive selection.")
5✔
261
                .arg(
5✔
262
                    clap::Arg::new(ConfigKey::SessionID.to_string())
5✔
263
                        .short('i')
5✔
264
                        .long("id")
5✔
265
                        .help("Session ID")
5✔
266
                        .required(false),
5✔
267
                ),
5✔
268
        )
5✔
269
        .subcommand(subcommand_sessions_delete());
5✔
270
}
5✔
271

272
pub fn build() -> Command {
5✔
273
    let commands_text = help_text()
5✔
274
        .split('\n')
5✔
275
        .map(|line| {
145✔
276
            if line.starts_with('-') {
145✔
277
                return format!("  {line}");
100✔
278
            }
45✔
279
            if line.starts_with("COMMANDS:")
45✔
280
                || line.starts_with("HOTKEYS:")
40✔
281
                || line.starts_with("CODE ACTIONS:")
35✔
282
            {
283
                return Paint::new(format!("CHAT {line}"))
15✔
284
                    .underline()
15✔
285
                    .bold()
15✔
286
                    .to_string();
15✔
287
            }
30✔
288
            return line.to_string();
30✔
289
        })
145✔
290
        .collect::<Vec<String>>()
5✔
291
        .join("\n");
5✔
292

5✔
293
    let about = format!(
5✔
294
        "{}\n\nVersion: {}\nCommit: {}",
5✔
295
        env!("CARGO_PKG_DESCRIPTION"),
5✔
296
        env!("CARGO_PKG_VERSION"),
5✔
297
        env!("VERGEN_GIT_DESCRIBE")
5✔
298
    );
5✔
299

5✔
300
    let themes = Themes::list();
5✔
301

5✔
302
    return Command::new("oatmeal")
5✔
303
        .about(about)
5✔
304
        .author(env!("CARGO_PKG_AUTHORS"))
5✔
305
        .version(env!("CARGO_PKG_VERSION"))
5✔
306
        .after_help(commands_text)
5✔
307
        .arg_required_else_help(false)
5✔
308
        .subcommand(subcommand_chat())
5✔
309
        .subcommand(subcommand_completions())
5✔
310
        .subcommand(subcommand_config())
5✔
311
        .subcommand(subcommand_debug())
5✔
312
        .subcommand(subcommand_sessions())
5✔
313
        .arg(arg_backend())
5✔
314
        .arg(arg_backend_health_check_timeout())
5✔
315
        .arg(arg_model())
5✔
316
        .arg(
5✔
317
            Arg::new(ConfigKey::ConfigFile.to_string())
5✔
318
                .short('c')
5✔
319
                .long(ConfigKey::ConfigFile.to_string())
5✔
320
                .env("OATMEAL_CONFIG_FILE")
5✔
321
                .num_args(1)
5✔
322
                .help(format!("Path to configuration file [default: {}]", Config::default(ConfigKey::ConfigFile)))
5✔
323
                .global(true)
5✔
324
        )
5✔
325
        .arg(
5✔
326
            Arg::new(ConfigKey::Editor.to_string())
5✔
327
                .short('e')
5✔
328
                .long(ConfigKey::Editor.to_string())
5✔
329
                .env("OATMEAL_EDITOR")
5✔
330
                .num_args(1)
5✔
331
                .help(format!("The editor to integrate with. [default: {}]", Config::default(ConfigKey::Editor)))
5✔
332
                .value_parser(PossibleValuesParser::new(EditorName::VARIANTS))
5✔
333
                .global(true),
5✔
334
        )
5✔
335
        .arg(
5✔
336
            Arg::new(ConfigKey::Theme.to_string())
5✔
337
                .short('t')
5✔
338
                .long(ConfigKey::Theme.to_string())
5✔
339
                .env("OATMEAL_THEME")
5✔
340
                .num_args(1)
5✔
341
                .help(format!("Sets code syntax highlighting theme. [default: {}]", Config::default(ConfigKey::Theme)))
5✔
342
                .value_parser(PossibleValuesParser::new(themes))
5✔
343
                .global(true),
5✔
344
        )
5✔
345
        .arg(
5✔
346
            Arg::new(ConfigKey::ThemeFile.to_string())
5✔
347
                .long(ConfigKey::ThemeFile.to_string())
5✔
348
                .env("OATMEAL_THEME_FILE")
5✔
349
                .num_args(1)
5✔
350
                .help(
5✔
351
                    "Absolute path to a TextMate tmTheme to use for code syntax highlighting."
5✔
352
                )
5✔
353
                .global(true),
5✔
354
        )
5✔
355
        .arg(
5✔
356
            Arg::new(ConfigKey::LangChainURL.to_string())
5✔
357
                .long(ConfigKey::LangChainURL.to_string())
5✔
358
                .env("OATMEAL_LANGCHAIN_URL")
5✔
359
                .num_args(1)
5✔
360
                .help(format!("LangChain Serve API URL when using the LangChain backend. [default: {}]", Config::default(ConfigKey::LangChainURL)))
5✔
361
                .global(true),
5✔
362
        )
5✔
363
        .arg(
5✔
364
            Arg::new(ConfigKey::OllamaURL.to_string())
5✔
365
                .long(ConfigKey::OllamaURL.to_string())
5✔
366
                .env("OATMEAL_OLLAMA_URL")
5✔
367
                .num_args(1)
5✔
368
                .help(format!("Ollama API URL when using the Ollama backend. [default: {}]", Config::default(ConfigKey::OllamaURL)))
5✔
369
                .global(true),
5✔
370
        )
5✔
371
        .arg(
5✔
372
            Arg::new(ConfigKey::OpenAiURL.to_string())
5✔
373
                .long(ConfigKey::OpenAiURL.to_string())
5✔
374
                .env("OATMEAL_OPENAI_URL")
5✔
375
                .num_args(1)
5✔
376
                .help(format!("OpenAI API URL when using the OpenAI backend. Can be swapped to a compatible proxy. [default: {}]", Config::default(ConfigKey::OpenAiURL)))
5✔
377
                .global(true),
5✔
378
        )
5✔
379
        .arg(
5✔
380
            Arg::new(ConfigKey::OpenAiToken.to_string())
5✔
381
                .long(ConfigKey::OpenAiToken.to_string())
5✔
382
                .env("OATMEAL_OPENAI_TOKEN")
5✔
383
                .num_args(1)
5✔
384
                .help("OpenAI API token when using the OpenAI backend.")
5✔
385
                .global(true),
5✔
386
        );
5✔
387
}
5✔
388

389
pub async fn parse() -> Result<bool> {
×
390
    let matches = build().get_matches();
×
391

×
392
    match matches.subcommand() {
×
393
        Some(("debug", debug_matches)) => {
×
394
            match debug_matches.subcommand() {
×
UNCOV
395
                Some(("syntaxes", _)) => {
×
396
                    println!("{}", Syntaxes::list().join("\n"));
×
397
                    return Ok(false);
×
398
                }
399
                Some(("themes", _)) => {
×
400
                    println!("{}", Themes::list().join("\n"));
×
401
                    return Ok(false);
×
402
                }
403
                Some(("log-path", _)) => {
×
404
                    let log_path = dirs::cache_dir().unwrap().join("oatmeal/debug.log");
×
405
                    println!("{}", log_path.to_str().unwrap());
×
UNCOV
406
                    return Ok(false);
×
407
                }
408
                Some(("enum-config", _)) => {
×
409
                    let res = ConfigKey::VARIANTS.join("\n");
×
UNCOV
410
                    println!("{}", res);
×
411
                    return Ok(false);
×
412
                }
413
                _ => {
414
                    subcommand_debug().print_long_help()?;
×
415
                    return Ok(false);
×
416
                }
417
            }
418
        }
419
        Some(("chat", subcmd_matches)) => {
×
420
            Config::load(build(), vec![&matches, subcmd_matches]).await?;
×
421
        }
422
        Some(("completions", subcmd_matches)) => {
×
UNCOV
423
            if let Some(completions) = subcmd_matches.get_one::<Shell>("shell").copied() {
×
424
                let mut app = build();
×
UNCOV
425
                print_completions(completions, &mut app);
×
UNCOV
426
            }
×
427
        }
428
        Some(("config", subcmd_matches)) => {
×
UNCOV
429
            match subcmd_matches.subcommand() {
×
UNCOV
430
                Some(("create", _)) => {
×
UNCOV
431
                    create_config_file().await?;
×
UNCOV
432
                    return Ok(false);
×
433
                }
434
                Some(("default", _)) => {
×
435
                    println!("{}", Config::serialize_default(build()));
×
436
                    return Ok(false);
×
437
                }
438
                Some(("path", _)) => {
×
439
                    println!("{}", Config::default(ConfigKey::ConfigFile));
×
440
                    return Ok(false);
×
441
                }
442
                _ => {
443
                    subcommand_config().print_long_help()?;
×
444
                    return Ok(false);
×
445
                }
446
            }
447
        }
UNCOV
448
        Some(("sessions", subcmd_matches)) => {
×
449
            match subcmd_matches.subcommand() {
×
450
                Some(("dir", _)) => {
×
451
                    let dir = Sessions::default().cache_dir.to_string_lossy().to_string();
×
452
                    println!("{dir}");
×
453
                    return Ok(false);
×
454
                }
455
                Some(("list", _)) => {
×
456
                    print_sessions_list().await?;
×
457
                    return Ok(false);
×
458
                }
459
                Some(("open", open_matches)) => {
×
460
                    Config::load(build(), vec![&matches, open_matches]).await?;
×
461
                    if let Some(session_id) = open_matches.get_one::<String>("session-id") {
×
462
                        load_config_from_session(session_id).await?;
×
463
                    } else {
464
                        load_config_from_session_interactive().await?;
×
465
                    }
466
                }
467
                Some(("delete", delete_matches)) => {
×
468
                    if let Some(session_id) = delete_matches.get_one::<String>("session-id") {
×
469
                        Sessions::default().delete(session_id).await?;
×
470
                        println!("Deleted session {session_id}");
×
471
                    } else if delete_matches.get_one::<bool>("all").is_some() {
×
472
                        Sessions::default().delete_all().await?;
×
473
                        println!("Deleted all sessions");
×
474
                    } else {
UNCOV
475
                        subcommand_sessions_delete().print_long_help()?;
×
476
                    }
477
                    return Ok(false);
×
478
                }
479
                _ => {
480
                    subcommand_sessions().print_long_help()?;
×
481
                    return Ok(false);
×
482
                }
483
            }
484
        }
485
        _ => {
486
            Config::load(build(), vec![&matches]).await?;
×
487
        }
488
    }
489

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