Skip to main content

innards/
inline_text.rs

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}