1use std::env;
2use std::path::PathBuf;
3
4use anyhow::{Context, Result, anyhow};
5
6use crate::config::{InnardsConfig, Keymap};
7
8mod buffer;
9mod editor;
10mod input;
11mod render;
12mod syntax;
13mod terminal;
14mod text_mode;
15
16#[cfg(test)]
17mod tests;
18
19use editor::Editor;
20use input::{Outcome, run_editor};
21use syntax::SyntaxHighlighter;
22use terminal::TerminalGuard;
23
24const DEFAULT_HEIGHT: u16 = 16;
25pub(super) const MIN_HEIGHT: u16 = 5;
26const DEFAULT_FILL_COLUMN: usize = 80;
27
28const INLINE_KEY_BINDINGS: &[(&str, &[&str])] = &[
29 ("quit", &["ctrl-x ctrl-c"]),
30 ("save", &["ctrl-x ctrl-s"]),
31 ("search_forward", &["ctrl-s"]),
32 ("search_reverse", &["ctrl-r"]),
33 ("cancel_search", &["esc", "ctrl-g"]),
34 ("finish_search", &["enter"]),
35 ("cancel_mark", &["ctrl-g"]),
36 ("set_mark", &["ctrl-space", "null"]),
37 ("undo", &["ctrl-/", "ctrl-_", "ctrl-7"]),
38 ("redo", &["ctrl-?"]),
39 ("line_start", &["ctrl-a", "home"]),
40 ("line_end", &["ctrl-e", "end"]),
41 ("word_left", &["alt-b", "ctrl-left"]),
42 ("word_right", &["alt-f", "ctrl-right"]),
43 ("char_left", &["ctrl-b", "left"]),
44 ("char_right", &["ctrl-f", "right"]),
45 ("line_up", &["ctrl-p", "up"]),
46 ("line_down", &["ctrl-n", "down"]),
47 ("page_up", &["alt-v", "pageup"]),
48 ("page_down", &["ctrl-v", "pagedown"]),
49 ("copy_region", &["alt-w"]),
50 ("kill_region", &["ctrl-w"]),
51 ("kill_to_eol", &["ctrl-k"]),
52 ("yank", &["ctrl-y"]),
53 ("delete_char", &["ctrl-d", "delete"]),
54 ("backspace", &["backspace"]),
55 ("insert_newline", &["enter"]),
56 ("insert_tab", &["tab"]),
57 ("shrink_height", &["alt-up"]),
58 ("grow_height", &["alt-down"]),
59 ("fullscreen", &["ctrl-x 1"]),
60 ("restore_inline", &["ctrl-x 0"]),
61 ("fill_paragraph", &["alt-q"]),
62 ("quit_view", &["esc", "q"]),
63];
64
65const INLINE_NORMAL_ACTIONS: &[&str] = &[
66 "quit",
67 "save",
68 "search_forward",
69 "search_reverse",
70 "cancel_mark",
71 "set_mark",
72 "undo",
73 "redo",
74 "line_start",
75 "line_end",
76 "word_left",
77 "word_right",
78 "char_left",
79 "char_right",
80 "line_up",
81 "line_down",
82 "page_up",
83 "page_down",
84 "copy_region",
85 "kill_region",
86 "kill_to_eol",
87 "yank",
88 "delete_char",
89 "backspace",
90 "insert_newline",
91 "insert_tab",
92 "shrink_height",
93 "grow_height",
94 "fullscreen",
95 "restore_inline",
96 "fill_paragraph",
97];
98const INLINE_VIEW_ACTIONS: &[&str] = &[
99 "quit",
100 "quit_view",
101 "search_forward",
102 "search_reverse",
103 "line_start",
104 "line_end",
105 "word_left",
106 "word_right",
107 "char_left",
108 "char_right",
109 "line_up",
110 "line_down",
111 "page_up",
112 "page_down",
113 "shrink_height",
114 "grow_height",
115 "fullscreen",
116 "restore_inline",
117];
118const INLINE_SEARCH_ACTIONS: &[&str] = &[
119 "quit",
120 "save",
121 "fullscreen",
122 "restore_inline",
123 "search_forward",
124 "search_reverse",
125 "cancel_search",
126 "finish_search",
127 "backspace",
128];
129
130#[derive(Clone, Copy, Debug, Eq, PartialEq)]
131pub enum Mode {
132 Edit,
133 View,
134}
135
136impl Mode {
137 fn title(self) -> &'static str {
138 match self {
139 Self::Edit => "inmacs",
140 Self::View => "inpage",
141 }
142 }
143
144 fn initial_status(self) -> &'static str {
145 match self {
146 Self::Edit => "Ctrl-S search Ctrl-R reverse-search Ctrl-X Ctrl-S save",
147 Self::View => "Ctrl-S search Ctrl-R reverse-search Ctrl-X Ctrl-C quit",
148 }
149 }
150
151 fn is_editable(self) -> bool {
152 matches!(self, Self::Edit)
153 }
154}
155
156pub fn run(mode: Mode) -> Result<()> {
157 let config = Config::parse(env::args().skip(1), mode)?;
158 let innards_config = InnardsConfig::load()?;
159 let fill_column = innards_config
160 .inmacs
161 .fill_column
162 .unwrap_or(DEFAULT_FILL_COLUMN);
163 let mut keymap = Keymap::from_defaults(INLINE_KEY_BINDINGS)?;
164 keymap.apply_overrides(&innards_config.keybindings.inline)?;
165
166 let mut app = Editor::open(
167 config.path.clone(),
168 config.line,
169 config.height,
170 fill_column,
171 mode,
172 )?;
173 let syntax = SyntaxHighlighter::new(&config.path)?;
174
175 let mut terminal = TerminalGuard::enter(config.height)?;
176 terminal
177 .terminal
178 .draw(|frame| render::draw(frame, &mut app, &syntax, mode))?;
179 let outcome = run_editor(&mut terminal, &mut app, &syntax, mode, &keymap)?;
180 drop(terminal);
181
182 match outcome {
183 Outcome::Quit => Ok(()),
184 }
185}
186
187struct Config {
188 path: PathBuf,
189 height: u16,
190 line: Option<usize>,
191}
192
193impl Config {
194 fn parse(args: impl Iterator<Item = String>, mode: Mode) -> Result<Self> {
195 let mut height = DEFAULT_HEIGHT;
196 let mut line = None;
197 let mut path = None;
198 let mut args = args.peekable();
199
200 while let Some(arg) = args.next() {
201 if arg == "--height" || arg == "-h" {
202 let value = args
203 .next()
204 .ok_or_else(|| anyhow!("{arg} requires a row count"))?;
205 height = value
206 .parse::<u16>()
207 .with_context(|| format!("invalid height: {value}"))?;
208 } else if let Some(value) = arg.strip_prefix("--height=") {
209 height = value
210 .parse::<u16>()
211 .with_context(|| format!("invalid height: {value}"))?;
212 } else if arg == "--line" {
213 let value = args
214 .next()
215 .ok_or_else(|| anyhow!("--line requires a line number"))?;
216 line = Some(parse_line_number(&value)?);
217 } else if let Some(value) = arg.strip_prefix("--line=") {
218 line = Some(parse_line_number(value)?);
219 } else if let Some(value) = arg.strip_prefix('+') {
220 if !value.is_empty() && value.chars().all(|ch| ch.is_ascii_digit()) {
221 line = Some(parse_line_number(value)?);
222 } else {
223 path = Some(PathBuf::from(arg));
224 }
225 } else if path.is_none() {
226 path = Some(PathBuf::from(arg));
227 } else {
228 return Err(anyhow!("unexpected argument: {arg}"));
229 }
230 }
231
232 let path =
233 path.ok_or_else(|| anyhow!("usage: {} [--height N] [+LINE] FILE", mode.title()))?;
234 Ok(Self { path, height, line })
235 }
236}
237
238fn parse_line_number(value: &str) -> Result<usize> {
239 let line = value
240 .parse::<usize>()
241 .with_context(|| format!("invalid line number: {value}"))?;
242 Ok(line.max(1))
243}