indigo_core/mode/
command.rs1use crate::{buffer::Buffer, editor::Editor, mode::prompt, window::WindowState};
2use anyhow::anyhow;
3use camino::Utf8PathBuf;
4use std::sync::Arc;
5
6#[cfg(feature = "arbitrary")]
7use arbitrary::Arbitrary;
8
9pub type Action = Command;
10
11#[derive(Clone, Debug)]
12pub enum Command {
13 Nop,
14 Echo { error: bool, message: Vec<String> },
15 Edit { path: Utf8PathBuf },
16 Write { path: Option<Utf8PathBuf> },
17 Quit { exit_code: Option<u8> },
18 QuitForce { exit_code: Option<u8> },
19 WriteQuit { exit_code: Option<u8> },
20 Readonly,
21 Panic,
22}
23
24#[cfg(feature = "arbitrary")]
25impl<'a> Arbitrary<'a> for Command {
26 fn arbitrary(_u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
27 Ok(Self::Nop)
29 }
30}
31
32pub fn handle_actions(editor: &mut Editor, actions: &[Action]) {
33 for action in actions {
34 handle_action(editor, action);
35 }
36}
37
38pub fn handle_action(editor: &mut Editor, action: &Action) {
39 handle_command(editor, action.clone());
40}
41
42pub fn enter_command_mode(editor: &mut Editor) {
43 prompt::enter(editor, "", |editor, input| match parse_command(input) {
44 Ok(command) => handle_command(editor, command),
45 Err(error) => editor.message = Some(Err(error.to_string())),
46 });
47}
48
49fn parse_command(command: &str) -> anyhow::Result<Command> {
50 use bpaf::{ParseFailure, Parser, construct, long, positional, pure};
51
52 let echo = {
53 let error = long("error").switch();
54 let message = positional("MESSAGE").many();
55 construct!(Command::Echo { error, message })
56 .to_options()
57 .command("echo")
58 };
59
60 let edit = {
61 let path = positional("PATH");
62 construct!(Command::Edit { path })
63 .to_options()
64 .command("edit")
65 .long("e")
66 };
67
68 let write = {
69 let path = positional("PATH").optional();
70 construct!(Command::Write { path })
71 .to_options()
72 .command("write")
73 .long("w")
74 };
75
76 let quit = {
77 let exit_code = positional("EXIT_CODE").optional();
78 construct!(Command::Quit { exit_code })
79 .to_options()
80 .command("quit")
81 .long("q")
82 };
83
84 let quit_force = {
85 let exit_code = positional("EXIT_CODE").optional();
86 construct!(Command::QuitForce { exit_code })
87 .to_options()
88 .command("quit!")
89 .long("q!")
90 };
91
92 let write_quit = {
93 let exit_code = positional("EXIT_CODE").optional();
94 construct!(Command::WriteQuit { exit_code })
95 .to_options()
96 .command("writequit")
97 .long("wq")
98 };
99
100 let readonly = pure(Command::Readonly)
101 .to_options()
102 .command("readonly")
103 .long("ro");
104
105 let panic = pure(Command::Panic).to_options().command("panic");
106
107 let parser = construct!([
108 echo, edit, write, quit, quit_force, write_quit, readonly, panic
109 ])
110 .to_options();
111
112 let Ok(args) = shell_words::split(command) else {
113 anyhow::bail!("Invalid command");
114 };
115
116 parser.run_inner(args.as_slice()).map_err(|failure| {
117 let message = match failure {
118 ParseFailure::Stderr(doc) => doc.monochrome(true),
119 ParseFailure::Stdout(doc, full) => doc.monochrome(full),
120 ParseFailure::Completion(s) => s,
121 };
122 anyhow!("{message}")
123 })
124}
125
126fn handle_command(editor: &mut Editor, command: Command) {
127 match command {
128 Command::Nop => {}
129 Command::Echo { error, message } => {
130 if error {
131 editor.message = Some(Err(message.join(" ")));
132 } else {
133 editor.message = Some(Ok(message.join(" ")));
134 }
135 }
136 Command::Edit { path } => {
137 if let Ok(buffer) = Buffer::open(&editor.fs, &path) {
139 let buffer_key = editor.insert_buffer(buffer);
143 let window_state = WindowState::new(buffer_key);
144 let window_key = editor.insert_window(window_state);
145 assert!(editor.focus_window(window_key));
146 } else {
149 editor.message = Some(Err(format!("Failed to open {path}")));
150 }
151 }
152 Command::Write { path } => {
153 let fs = Arc::clone(&editor.fs);
154 let result = if let Some(path) = path {
155 editor.focused_buffer_mut().save_as(&fs, path)
156 } else {
157 editor.focused_buffer_mut().save(&fs)
158 };
159 if result.is_err() {
160 editor.message = Some(Err(String::from("Failed to save")));
161 }
162 }
163 Command::Quit { exit_code } => {
164 let mut modified = None;
165 for (_, buffer) in editor.buffers() {
166 if buffer.is_modified().unwrap_or(false) {
167 modified = Some(
168 buffer
169 .path()
170 .map_or("a scratch buffer", |path| path.as_str()),
171 );
172 break;
173 }
174 }
175 if let Some(modified) = modified {
176 editor.message = Some(Err(format!("Unsaved changes in {modified}")));
177 } else {
178 editor.exit(exit_code.unwrap_or(0));
179 }
180 }
181 Command::QuitForce { exit_code } => {
182 editor.exit(exit_code.unwrap_or(0));
183 }
184 Command::WriteQuit { exit_code } => {
185 let fs = Arc::clone(&editor.fs);
186 if editor.focused_buffer_mut().save(&fs).is_ok() {
187 editor.exit(exit_code.unwrap_or(0));
188 } else {
189 editor.message = Some(Err(String::from("Failed to save")));
190 }
191 }
192 Command::Readonly => {
193 let new_readonly = !editor.focused_buffer().text.readonly;
194 editor.focused_buffer_mut().text.readonly = new_readonly;
195 if new_readonly {
196 editor.message = Some(Ok(String::from("Buffer is now readonly")));
197 } else {
198 editor.message = Some(Ok(String::from("Buffer is no longer readonly")));
199 }
200 }
201 Command::Panic => panic!(),
202 }
203}