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

dustinblackman / oatmeal / 7067397165

02 Dec 2023 02:12AM UTC coverage: 49.261%. Remained the same
7067397165

push

github

dustinblackman
feat: Release v0.6.0

1066 of 2164 relevant lines covered (49.26%)

16.11 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 owo_colors::OwoColorize;
15
use owo_colors::Stream;
16

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

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

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

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

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

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

52
    return res;
×
53
}
×
54

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

×
65
    sessions.reverse();
×
66

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

291
pub async fn parse() -> Result<bool> {
×
292
    let matches = build().get_matches();
×
293

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

359
    Config::set(
×
360
        ConfigKey::Editor,
×
361
        matches.get_one::<String>("editor").unwrap(),
×
362
    );
×
363
    Config::set(
×
364
        ConfigKey::Theme,
×
365
        matches.get_one::<String>("theme").unwrap(),
×
366
    );
×
367
    Config::set(
×
368
        ConfigKey::OpenAIURL,
×
369
        matches.get_one::<String>("openai-url").unwrap(),
×
370
    );
×
371

372
    if let Some(theme_file) = matches.get_one::<String>("theme-file") {
×
373
        Config::set(ConfigKey::ThemeFile, theme_file);
×
374
    }
×
375

376
    if let Some(openai_token) = matches.get_one::<String>("openai-token") {
×
377
        Config::set(ConfigKey::OpenAIToken, openai_token);
×
378
    }
×
379

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