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

dustinblackman / oatmeal / 7216917872

15 Dec 2023 01:51AM UTC coverage: 45.689%. Remained the same
7216917872

push

github

dustinblackman
feat: Release v0.9.1

1118 of 2447 relevant lines covered (45.69%)

17.97 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")
×
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")
×
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(
×
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)
×
201
        .help("The initial model on a backend to consume")
×
202
        .default_value("llama2:latest");
×
203
}
×
204

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

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

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

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

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

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

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

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

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

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

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

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

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

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