Skip to content

Commit 3c16aff

Browse files
authored
feat: add -c/--code to generate only code (#327)
1 parent d275c33 commit 3c16aff

File tree

6 files changed

+67
-17
lines changed

6 files changed

+67
-17
lines changed

README.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,11 @@ Options:
311311
-r, --role <ROLE> Choose a role
312312
-s, --session [<SESSION>] Create or reuse a session
313313
-e, --execute Execute commands using natural language
314+
-c, --code Generate only code
314315
-f, --file <FILE>... Attach files to the message to be sent
315316
-H, --no-highlight Disable syntax highlighting
316317
-S, --no-stream No stream output
317-
-w, --wrap <WRAP> Specify the text-wrapping mode (no*, auto, <max-width>)
318+
-w, --wrap <WRAP> Specify the text-wrapping mode (no, auto, <max-width>)
318319
--light-theme Use light theme
319320
--dry-run Run in dry run mode
320321
--info Print related information
@@ -328,18 +329,16 @@ Options:
328329
Here are some practical examples:
329330
330331
```sh
331-
aichat -s # Start REPL with a new temp session
332+
aichat -s # Start REPL with a new session
332333
aichat -s temp # Reuse temp session
333334
aichat -r shell -s # Create a session with a role
334335
aichat -m openai:gpt-4-32k -s # Create a session with a model
335-
aichat -s sh unzip a file # Run session in command mode
336+
aichat -s temp unzip a file # Run session in command mode
336337
337338
aichat -r shell unzip a file # Use role in command mode
338339
aichat -s shell unzip a file # Use session in command mode
339340
340-
cat config.json | aichat convert to yaml # Read stdin
341-
cat config.json | aichat -r convert:yaml # Read stdin with a role
342-
cat config.json | aichat -s i18n # Read stdin with a session
341+
cat data.toml | aichat -c to json # Read stdin
343342
344343
aichat --file a.png b.png -- diff images # Attach files
345344
aichat --file screenshot.png -r ocr # Attach files with a role
@@ -352,7 +351,7 @@ aichat --info # system-wide information
352351
aichat -s temp --info # Show session details
353352
aichat -r shell --info # Show role info
354353
355-
$(echo "$data" | aichat -S -H to json) # Use aichat in a script
354+
$(echo "$data" | aichat -c to json) # Use aichat in a script
356355
```
357356

358357
### Execute commands using natural language
@@ -399,7 +398,7 @@ This is a **very handy feature**, which allows you to use `aichat` shell complet
399398

400399
![aichat-integration](https://github.com/sigoden/aichat/assets/4012553/873ebf23-226c-412e-a34f-c5aaa7017524)
401400

402-
To install shell integration, go to [./scripts/shell-integration](./scripts/shell-integration/) to download the script and source the script in rc file. After that restart your shell. You can invoke the completion with `alt+e` hotkey.
401+
To install shell integration, go to [./scripts/shell-integration](https://github.com/sigoden/aichat/tree/main/scripts/shell-integration) to download the script and source the script in rc file. After that restart your shell. You can invoke the completion with `alt+e` hotkey.
403402

404403
## License
405404

src/cli.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ pub struct Cli {
1515
/// Execute commands using natural language
1616
#[clap(short = 'e', long)]
1717
pub execute: bool,
18+
/// Generate only code
19+
#[clap(short = 'c', long)]
20+
pub code: bool,
1821
/// Attach files to the message to be sent.
1922
#[clap(short = 'f', long, num_args = 1.., value_name = "FILE")]
2023
pub file: Option<Vec<String>>,

src/config/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,11 @@ impl Config {
288288
self.set_role_obj(role)
289289
}
290290

291+
pub fn set_code_role(&mut self) -> Result<()> {
292+
let role = Role::for_code();
293+
self.set_role_obj(role)
294+
}
295+
291296
pub fn set_role_obj(&mut self, role: Role) -> Result<()> {
292297
if let Some(session) = self.session.as_mut() {
293298
session.update_role(Some(role.clone()))?;

src/config/role.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ impl Role {
2929
_ => "&&",
3030
};
3131
Self {
32-
name: "__for_execute__".into(),
32+
name: "__execute__".into(),
3333
prompt: format!(
3434
r#"Provide only {shell} commands for {os} without any description.
3535
If there is a lack of details, provide most logical solution.
@@ -44,7 +44,7 @@ Do not provide markdown formatting such as ```"#
4444

4545
pub fn for_describe() -> Self {
4646
Self {
47-
name: "__for_describe__".into(),
47+
name: "__describe__".into(),
4848
prompt: r#"Provide a terse, single sentence description of the given shell command.
4949
Describe each argument and option of the command.
5050
Provide short responses in about 80 words.
@@ -54,6 +54,20 @@ APPLY MARKDOWN formatting when possible."#
5454
}
5555
}
5656

57+
pub fn for_code() -> Self {
58+
Self {
59+
name: "__code__".into(),
60+
prompt: r#"Provide only code as output without any description.
61+
Provide only code in plain text format without Markdown formatting.
62+
Do not include symbols such as ``` or ```python.
63+
If there is a lack of details, provide most logical solution.
64+
You are not allowed to ask for more details.
65+
For example if the prompt is "Hello world Python", you should return "print('Hello world')"."#
66+
.into(),
67+
temperature: None,
68+
}
69+
}
70+
5771
pub fn info(&self) -> Result<String> {
5872
let output = serde_yaml::to_string(&self)
5973
.with_context(|| format!("Unable to show info about role {}", &self.name))?;

src/main.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ mod utils;
1111

1212
use crate::cli::Cli;
1313
use crate::config::{Config, GlobalConfig};
14-
use crate::utils::run_command;
14+
use crate::utils::{extract_block, run_command};
1515

1616
use anyhow::{bail, Result};
1717
use clap::Parser;
@@ -65,6 +65,8 @@ fn main() -> Result<()> {
6565
} else {
6666
if let Some(name) = &cli.role {
6767
config.write().set_role(name)?;
68+
} else if cli.code {
69+
config.write().set_code_role()?;
6870
}
6971
if let Some(session) = &cli.session {
7072
config
@@ -95,7 +97,7 @@ fn main() -> Result<()> {
9597
}
9698
config.write().prelude()?;
9799
if let Err(err) = match text {
98-
Some(text) => start_directive(&config, &text, cli.file, cli.no_stream),
100+
Some(text) => start_directive(&config, &text, cli.file, cli.no_stream, cli.code),
99101
None => start_interactive(&config),
100102
} {
101103
let highlight = stderr().is_terminal() && config.read().highlight;
@@ -109,6 +111,7 @@ fn start_directive(
109111
text: &str,
110112
include: Option<Vec<String>>,
111113
no_stream: bool,
114+
code_mode: bool,
112115
) -> Result<()> {
113116
if let Some(session) = &config.read().session {
114117
session.guard_save()?;
@@ -117,14 +120,19 @@ fn start_directive(
117120
let mut client = init_client(config)?;
118121
ensure_model_capabilities(client.as_mut(), input.required_capabilities())?;
119122
config.read().maybe_print_send_tokens(&input);
120-
let output = if no_stream {
123+
let output = if !stdout().is_terminal() || no_stream {
121124
let output = client.send_message(input.clone())?;
122-
if stdout().is_terminal() {
125+
let to_print = if code_mode && output.trim_start().starts_with("```") {
126+
extract_block(&output)
127+
} else {
128+
output.clone()
129+
};
130+
if no_stream {
123131
let render_options = config.read().get_render_options()?;
124132
let mut markdown_render = MarkdownRender::init(render_options)?;
125-
println!("{}", markdown_render.render(&output).trim());
133+
println!("{}", markdown_render.render(&to_print).trim());
126134
} else {
127-
println!("{}", output);
135+
println!("{}", to_print);
128136
}
129137
output
130138
} else {
@@ -144,7 +152,10 @@ fn execute(config: &GlobalConfig, text: &str) -> Result<()> {
144152
let input = Input::from_str(text);
145153
let client = init_client(config)?;
146154
config.read().maybe_print_send_tokens(&input);
147-
let eval_str = client.send_message(input.clone())?;
155+
let mut eval_str = client.send_message(input.clone())?;
156+
if eval_str.contains("```") {
157+
eval_str = extract_block(&eval_str);
158+
}
148159
config.write().save_message(input, &eval_str)?;
149160
let render_options = config.read().get_render_options()?;
150161
let mut markdown_render = MarkdownRender::init(render_options)?;

src/utils/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ pub use self::prompt_input::*;
1010
pub use self::render_prompt::render_prompt;
1111
pub use self::tiktoken::cl100k_base_singleton;
1212

13+
use fancy_regex::Regex;
14+
use lazy_static::lazy_static;
1315
use sha2::{Digest, Sha256};
1416
use std::env;
1517
use std::process::Command;
1618

19+
lazy_static! {
20+
static ref CODE_BLOCK_RE: Regex = Regex::new(r"(?ms)```\w*(.*?)```").unwrap();
21+
}
22+
1723
pub fn now() -> String {
1824
let now = chrono::Local::now();
1925
now.to_rfc3339_opts(chrono::SecondsFormat::Secs, false)
@@ -150,6 +156,18 @@ pub fn run_command(eval_str: &str) -> anyhow::Result<i32> {
150156
Ok(status.code().unwrap_or_default())
151157
}
152158

159+
pub fn extract_block(input: &str) -> String {
160+
let output: String = CODE_BLOCK_RE
161+
.captures_iter(input)
162+
.filter_map(|m| {
163+
m.ok()
164+
.and_then(|cap| cap.get(1))
165+
.map(|m| String::from(m.as_str()))
166+
})
167+
.collect();
168+
output.trim().to_string()
169+
}
170+
153171
#[cfg(test)]
154172
mod tests {
155173
use super::*;

0 commit comments

Comments
 (0)