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

dustinblackman / oatmeal / 7109538031

06 Dec 2023 02:49AM UTC coverage: 47.535% (-0.6%) from 48.107%
7109538031

push

github

dustinblackman
feat: STDIN

0 of 28 new or added lines in 4 files covered. (0.0%)

1 existing line in 1 file now uncovered.

1080 of 2272 relevant lines covered (47.54%)

15.37 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::io;
2

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

16
use crate::config::Config;
17
use crate::config::ConfigKey;
18
use crate::domain::models::Session;
19
use crate::domain::services::actions::help_text;
20
use crate::domain::services::Sessions;
21
use crate::domain::services::Themes;
22

23
fn print_completions<G: Generator>(gen: G, cmd: &mut Command) {
×
24
    generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout());
×
25
    std::process::exit(0);
×
26
}
27

28
fn format_session(session: &Session) -> String {
×
29
    let mut res = format!(
×
30
        "- (ID: {}) {}, Model: {}",
×
31
        session.id, session.timestamp, session.state.backend_model,
×
32
    );
×
33

×
34
    if !session.state.editor_language.is_empty() {
×
35
        res = format!("{res}, Lang: {}", session.state.editor_language)
×
36
    }
×
37

38
    if !session.state.messages.is_empty() {
×
39
        let mut line = session.state.messages[0]
×
40
            .text
×
41
            .split('\n')
×
42
            .collect::<Vec<_>>()[0]
×
43
            .to_string();
×
44

×
45
        if line.len() >= 70 {
×
46
            line = format!("{}...", &line[..67]);
×
47
        }
×
48
        res = format!("{res}, {line}");
×
49
    }
×
50

51
    return res;
×
52
}
×
53

54
async fn print_sessions_list() -> Result<()> {
×
55
    let mut sessions = Sessions::default()
×
56
        .list()
×
57
        .await?
×
58
        .iter()
×
59
        .map(|session| {
×
60
            return format_session(session);
×
61
        })
×
62
        .collect::<Vec<String>>();
×
63

×
64
    sessions.reverse();
×
65

×
66
    if sessions.is_empty() {
×
67
        println!("There are no sessions available. You should start your first one!");
×
68
    } else {
×
69
        println!("{}", sessions.join("\n"));
×
70
    }
×
71

72
    return Ok(());
×
73
}
×
74

75
async fn load_config_from_session(session_id: &str) -> Result<()> {
×
76
    let session = Sessions::default().load(session_id).await?;
×
77
    Config::set(ConfigKey::Backend, &session.state.backend_name);
×
78
    Config::set(ConfigKey::Model, &session.state.backend_model);
×
79
    Config::set(ConfigKey::SessionID, session_id);
×
80

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

84
async fn load_config_from_session_interactive() -> Result<()> {
×
85
    let mut sessions = Sessions::default().list().await?;
×
86
    sessions.reverse();
×
87

×
88
    if sessions.is_empty() {
×
89
        println!("There are no sessions available. You should start your first one!");
×
90
        return Ok(());
×
91
    }
×
92

×
93
    let session_options = sessions
×
94
        .iter()
×
95
        .map(|session| {
×
96
            return format_session(session);
×
97
        })
×
98
        .collect::<Vec<String>>();
×
99

100
    let idx = Select::with_theme(&ColorfulTheme::default())
×
101
        .with_prompt("Which session would you like to load?")
×
102
        .default(0)
×
103
        .items(&session_options)
×
104
        .interact_opt()?
×
105
        .unwrap();
×
106

×
107
    load_config_from_session(&sessions[idx].id).await?;
×
108

109
    return Ok(());
×
110
}
×
111

112
fn subcommand_completions() -> Command {
×
113
    return Command::new("completions")
×
114
        .about("Generates shell completions")
×
115
        .arg(
×
116
            clap::Arg::new("shell")
×
117
                .short('s')
×
118
                .long("shell")
×
119
                .help("Which shell to generate completions for")
×
120
                .action(ArgAction::Set)
×
121
                .value_parser(value_parser!(Shell))
×
122
                .required(true),
×
123
        );
×
124
}
×
125

126
fn subcommand_sessions_delete() -> Command {
×
127
    return Command::new("delete")
×
128
        .about("Delete one or all sessions")
×
129
        .arg(
×
130
            clap::Arg::new("session-id")
×
131
                .short('i')
×
132
                .long("id")
×
133
                .help("Session ID")
×
134
                .num_args(1),
×
135
        )
×
136
        .arg(
×
137
            clap::Arg::new("all")
×
138
                .long("all")
×
139
                .help("Delete all sessions")
×
140
                .num_args(0),
×
141
        )
×
142
        .group(
×
143
            ArgGroup::new("delete-args")
×
144
                .args(["session-id", "all"])
×
145
                .required(true),
×
146
        );
×
147
}
×
148

149
fn arg_backend() -> Arg {
×
150
    return Arg::new("backend")
×
151
        .short('b')
×
152
        .long("backend")
×
153
        .env("OATMEAL_BACKEND")
×
154
        .num_args(1)
×
155
        .help(
×
156
            "The initial backend hosting a model to connect to. [Possible values: ollama, openai]",
×
157
        )
×
158
        .default_value("ollama");
×
159
}
×
160

161
fn arg_model() -> Arg {
×
162
    return Arg::new("model")
×
163
        .short('m')
×
164
        .long("model")
×
165
        .env("OATMEAL_MODEL")
×
166
        .num_args(1)
×
167
        .help("The initial model on a backend to consume")
×
168
        .default_value("llama2:latest");
×
169
}
×
170

171
fn subcommand_chat() -> Command {
×
172
    return Command::new("chat")
×
173
        .about("Start a new chat session")
×
174
        .arg(arg_backend())
×
175
        .arg(arg_model());
×
176
}
×
177

178
fn subcommand_sessions() -> Command {
×
179
    return Command::new("sessions")
×
180
        .about("Manage past chat sessions")
×
181
        .arg_required_else_help(true)
×
182
        .subcommand(Command::new("dir").about("Print the sessions cache directory path"))
×
183
        .subcommand(Command::new("list").about("List all previous sessions with their ids and models"))
×
184
        .subcommand(
×
185
            Command::new("open")
×
186
                .about("Open a previous session by ID. Omit passing any session ID to load an interactive selection")
×
187
                .arg(
×
188
                    clap::Arg::new("session-id")
×
189
                        .short('i')
×
190
                        .long("id")
×
191
                        .help("Session ID")
×
192
                        .required(false),
×
193
                ),
×
194
        )
×
195
        .subcommand(subcommand_sessions_delete());
×
196
}
×
197

198
fn build() -> Command {
×
199
    let commands_text = help_text()
×
200
        .split('\n')
×
201
        .map(|line| {
×
202
            if line.starts_with('-') {
×
203
                return format!("  {line}");
×
204
            }
×
205
            if line.starts_with("COMMANDS:")
×
206
                || line.starts_with("HOTKEYS:")
×
207
                || line.starts_with("CODE ACTIONS:")
×
208
            {
209
                return Paint::new(format!("CHAT {line}"))
×
210
                    .underline()
×
211
                    .bold()
×
212
                    .to_string();
×
213
            }
×
214
            return line.to_string();
×
215
        })
×
216
        .collect::<Vec<String>>()
×
217
        .join("\n");
×
218

×
219
    let about = format!(
×
220
        "{}\n\nVersion: {}\nCommit: {}",
×
221
        env!("CARGO_PKG_DESCRIPTION"),
×
222
        env!("CARGO_PKG_VERSION"),
×
223
        env!("VERGEN_GIT_DESCRIBE")
×
224
    );
×
225
    let themes = Themes::list().join(", ");
×
226

×
227
    return Command::new("oatmeal")
×
228
        .about(about)
×
229
        .author(env!("CARGO_PKG_AUTHORS"))
×
230
        .version(env!("CARGO_PKG_VERSION"))
×
231
        .after_help(commands_text)
×
232
        .arg_required_else_help(false)
×
233
        .subcommand(subcommand_chat())
×
234
        .subcommand(subcommand_completions())
×
235
        .subcommand(subcommand_sessions())
×
236
        .arg(arg_backend())
×
237
        .arg(arg_model())
×
238
        .arg(
×
239
            Arg::new("editor")
×
240
                .short('e')
×
241
                .long("editor")
×
242
                .env("OATMEAL_EDITOR")
×
243
                .num_args(1)
×
244
                .help("The editor to integrate with. [Possible values: clipboard, neovim]")
×
245
                .default_value("clipboard")
×
246
                .global(true),
×
247
        )
×
248
        .arg(
×
249
            Arg::new("theme")
×
250
                .short('t')
×
251
                .long("theme")
×
252
                .env("OATMEAL_THEME")
×
253
                .num_args(1)
×
254
                .help(format!(
×
255
                    "Sets code syntax highlighting theme. [Possible values: {themes}]"
×
256
                ))
×
257
                .default_value("base16-onedark")
×
258
                .global(true),
×
259
        )
×
260
        .arg(
×
261
            Arg::new("theme-file")
×
262
                .long("theme-file")
×
263
                .env("OATMEAL_THEME_FILE")
×
264
                .num_args(1)
×
265
                .help(
×
266
                    "Absolute path to a TextMate tmTheme to use for code syntax highlighting"
×
267
                )
×
268
                .global(true),
×
269
        )
×
270
        .arg(
×
271
            Arg::new("ollama-url")
×
272
                .long("ollama-url")
×
273
                .env("OATMEAL_OLLAMA_URL")
×
274
                .num_args(1)
×
275
                .help("Ollama API URL when using the Ollama backend")
×
276
                .default_value("http://localhost:11434")
×
277
                .global(true),
×
278
        )
×
279
        .arg(
×
280
            Arg::new("openai-url")
×
281
                .long("openai-url")
×
282
                .env("OATMEAL_OPENAI_URL")
×
283
                .num_args(1)
×
284
                .help("OpenAI API URL when using the OpenAI backend. Can be swapped to a compatiable proxy")
×
285
                .default_value("https://api.openai.com")
×
286
                .global(true),
×
287
        )
×
288
        .arg(
×
289
            Arg::new("openai-token")
×
290
                .long("openai-token")
×
291
                .env("OATMEAL_OPENAI_TOKEN")
×
292
                .num_args(1)
×
293
                .help("OpenAI API token when using the OpenAI backend")
×
294
                .global(true),
×
NEW
295
        )
×
NEW
296
        .arg(
×
NEW
297
            Arg::new("stdin")
×
NEW
298
                .long("stdin")
×
NEW
299
                .help("Read your first message for the backend from stdin (cat question.txt | oatmeal --stdin)")
×
NEW
300
                .num_args(0)
×
NEW
301
                .global(true),
×
302
        );
×
303
}
×
304

305
pub async fn parse() -> Result<bool> {
×
306
    let matches = build().get_matches();
×
307

×
308
    match matches.subcommand() {
×
309
        Some(("chat", subcmd_matches)) => {
×
310
            Config::set(
×
311
                ConfigKey::Backend,
×
312
                subcmd_matches.get_one::<String>("backend").unwrap(),
×
313
            );
×
314
            Config::set(
×
315
                ConfigKey::Model,
×
316
                subcmd_matches.get_one::<String>("model").unwrap(),
×
317
            );
×
318
        }
×
319
        Some(("completions", subcmd_matches)) => {
×
320
            if let Some(completions) = subcmd_matches.get_one::<Shell>("shell").copied() {
×
321
                let mut app = build();
×
322
                print_completions(completions, &mut app);
×
323
            }
×
324
        }
325
        Some(("sessions", subcmd_matches)) => {
×
326
            match subcmd_matches.subcommand() {
×
327
                Some(("dir", _)) => {
×
328
                    let dir = Sessions::default().cache_dir.to_string_lossy().to_string();
×
329
                    println!("{dir}");
×
330
                    return Ok(false);
×
331
                }
332
                Some(("list", _)) => {
×
333
                    print_sessions_list().await?;
×
334
                    return Ok(false);
×
335
                }
336
                Some(("open", open_matches)) => {
×
337
                    if let Some(session_id) = open_matches.get_one::<String>("session-id") {
×
338
                        load_config_from_session(session_id).await?;
×
339
                    } else {
340
                        load_config_from_session_interactive().await?;
×
341
                    }
342
                }
343
                Some(("delete", delete_matches)) => {
×
344
                    if let Some(session_id) = delete_matches.get_one::<String>("session-id") {
×
345
                        Sessions::default().delete(session_id).await?;
×
346
                        println!("Deleted session {session_id}");
×
347
                    } else if delete_matches.get_one::<bool>("all").is_some() {
×
348
                        Sessions::default().delete_all().await?;
×
349
                        println!("Deleted all sessions");
×
350
                    } else {
351
                        subcommand_sessions_delete().print_long_help()?;
×
352
                    }
353
                    return Ok(false);
×
354
                }
355
                _ => {
356
                    subcommand_sessions().print_long_help()?;
×
357
                    return Ok(false);
×
358
                }
359
            }
360
        }
361
        _ => {
×
362
            Config::set(
×
363
                ConfigKey::Backend,
×
364
                matches.get_one::<String>("backend").unwrap(),
×
365
            );
×
366
            Config::set(
×
367
                ConfigKey::Model,
×
368
                matches.get_one::<String>("model").unwrap(),
×
369
            );
×
370
        }
×
371
    }
372

373
    Config::set(
×
374
        ConfigKey::Editor,
×
375
        matches.get_one::<String>("editor").unwrap(),
×
376
    );
×
377
    Config::set(
×
378
        ConfigKey::Theme,
×
379
        matches.get_one::<String>("theme").unwrap(),
×
380
    );
×
381
    Config::set(
×
382
        ConfigKey::OllamaURL,
×
383
        matches.get_one::<String>("ollama-url").unwrap(),
×
384
    );
×
385
    Config::set(
×
386
        ConfigKey::OpenAIURL,
×
387
        matches.get_one::<String>("openai-url").unwrap(),
×
388
    );
×
389

390
    if let Some(theme_file) = matches.get_one::<String>("theme-file") {
×
391
        Config::set(ConfigKey::ThemeFile, theme_file);
×
392
    }
×
393

394
    if let Some(openai_token) = matches.get_one::<String>("openai-token") {
×
395
        Config::set(ConfigKey::OpenAIToken, openai_token);
×
396
    }
×
397

NEW
398
    if let Some(use_stdin) = matches.get_one::<bool>("stdin") {
×
NEW
399
        if *use_stdin {
×
NEW
400
            Config::set(ConfigKey::FirstMessageSTDIN, "true");
×
NEW
401
        }
×
NEW
402
    }
×
403

404
    tracing::debug!(
×
405
        backend = Config::get(ConfigKey::Backend),
×
406
        editor = Config::get(ConfigKey::Editor),
×
407
        model = Config::get(ConfigKey::Model),
×
408
        theme = Config::get(ConfigKey::Theme),
×
409
        theme_file = Config::get(ConfigKey::ThemeFile),
×
410
        "Config"
×
411
    );
×
412

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