Line | Branch | Exec | Source |
---|---|---|---|
1 | #include <errno.h> | ||
2 | #include <langinfo.h> | ||
3 | #include <locale.h> | ||
4 | #include <stdio.h> | ||
5 | #include <stdlib.h> | ||
6 | #include <sys/stat.h> | ||
7 | #include <unistd.h> | ||
8 | #include "editor.h" | ||
9 | #include "bind.h" | ||
10 | #include "bookmark.h" | ||
11 | #include "compiler.h" | ||
12 | #include "encoding.h" | ||
13 | #include "file-option.h" | ||
14 | #include "filetype.h" | ||
15 | #include "lock.h" | ||
16 | #include "signals.h" | ||
17 | #include "syntax/syntax.h" | ||
18 | #include "terminal/color.h" | ||
19 | #include "terminal/input.h" | ||
20 | #include "terminal/key.h" | ||
21 | #include "terminal/output.h" | ||
22 | #include "terminal/paste.h" | ||
23 | #include "ui.h" | ||
24 | #include "util/exitcode.h" | ||
25 | #include "util/intern.h" | ||
26 | #include "util/intmap.h" | ||
27 | #include "util/log.h" | ||
28 | #include "util/xmalloc.h" | ||
29 | #include "util/xstdio.h" | ||
30 | #include "version.h" | ||
31 | |||
32 | 9 | static void set_and_check_locale(void) | |
33 | { | ||
34 | 9 | const char *default_locale = setlocale(LC_CTYPE, ""); | |
35 |
1/2✓ Branch 0 (3→4) taken 9 times.
✗ Branch 1 (3→8) not taken.
|
9 | if (likely(default_locale)) { |
36 | 9 | const char *codeset = nl_langinfo(CODESET); | |
37 | 9 | LOG_INFO("locale: %s (codeset: %s)", default_locale, codeset); | |
38 |
1/2✗ Branch 0 (7→11) not taken.
✓ Branch 1 (7→19) taken 9 times.
|
9 | if (likely(lookup_encoding(codeset) == UTF8)) { |
39 | return; | ||
40 | } | ||
41 | } else { | ||
42 | ✗ | LOG_ERROR("failed to set default locale"); | |
43 | } | ||
44 | |||
45 | static const char fallbacks[][12] = {"C.UTF-8", "en_US.UTF-8"}; | ||
46 | const char *fallback = NULL; | ||
47 | ✗ | for (size_t i = 0; i < ARRAYLEN(fallbacks) && !fallback; i++) { | |
48 | ✗ | fallback = setlocale(LC_CTYPE, fallbacks[i]); | |
49 | } | ||
50 | ✗ | if (fallback) { | |
51 | ✗ | LOG_NOTICE("using fallback locale for LC_CTYPE: %s", fallback); | |
52 | ✗ | return; | |
53 | } | ||
54 | |||
55 | ✗ | LOG_ERROR("no UTF-8 fallback locales found"); | |
56 | ✗ | fputs("setlocale() failed\n", stderr); | |
57 | ✗ | exit(EC_CONFIG_ERROR); | |
58 | } | ||
59 | |||
60 | // Get permissions mask for new files | ||
61 | 9 | static mode_t get_umask(void) | |
62 | { | ||
63 | 9 | mode_t old = umask(0); // Get by setting a dummy value | |
64 | 9 | umask(old); // Restore previous value | |
65 | 9 | return old; | |
66 | } | ||
67 | |||
68 | 9 | EditorState *init_editor_state(EditorFlags flags) | |
69 | { | ||
70 | 9 | EditorState *e = xmalloc(sizeof(*e)); | |
71 | 18 | *e = (EditorState) { | |
72 | .status = EDITOR_INITIALIZING, | ||
73 | .flags = flags, | ||
74 | .command_history = { | ||
75 | .max_entries = 512, | ||
76 | }, | ||
77 | .search_history = { | ||
78 | .max_entries = 128, | ||
79 | }, | ||
80 | .terminal = { | ||
81 | .obuf = TERM_OUTPUT_INIT, | ||
82 | }, | ||
83 | .cursor_styles = { | ||
84 | [CURSOR_MODE_DEFAULT] = {.type = CURSOR_DEFAULT, .color = COLOR_DEFAULT}, | ||
85 | [CURSOR_MODE_INSERT] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
86 | [CURSOR_MODE_OVERWRITE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
87 | [CURSOR_MODE_CMDLINE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
88 | }, | ||
89 | .options = { | ||
90 | .auto_indent = true, | ||
91 | .detect_indent = 0, | ||
92 | .editorconfig = false, | ||
93 | .emulate_tab = false, | ||
94 | .expand_tab = false, | ||
95 | .file_history = true, | ||
96 | .indent_width = 8, | ||
97 | .overwrite = false, | ||
98 | .save_unmodified = SAVE_FULL, | ||
99 | .syntax = true, | ||
100 | .tab_width = 8, | ||
101 | .text_width = 72, | ||
102 | .ws_error = WSE_SPECIAL, | ||
103 | |||
104 | // Global-only options | ||
105 | .case_sensitive_search = CSS_TRUE, | ||
106 | .crlf_newlines = false, | ||
107 | .display_special = false, | ||
108 | .esc_timeout = 100, | ||
109 | .filesize_limit = 250, | ||
110 | .lock_files = true, | ||
111 | .optimize_true_color = true, | ||
112 | .scroll_margin = 0, | ||
113 | .select_cursor_char = true, | ||
114 | .set_window_title = false, | ||
115 | .show_line_numbers = false, | ||
116 | 9 | .statusline_left = str_intern(" %f%s%m%s%r%s%M"), | |
117 | 9 | .statusline_right = str_intern(" %y,%X %u %o %E%s%b%s%n %t %p "), | |
118 | .tab_bar = true, | ||
119 | .utf8_bom = false, | ||
120 | .window_separator = WINSEP_BAR, | ||
121 | .msg_compile = 0, | ||
122 | .msg_tag = 0, | ||
123 | } | ||
124 | }; | ||
125 | |||
126 | 9 | sanity_check_global_options(&e->err, &e->options); | |
127 | |||
128 | 9 | const char *home = getenv("HOME"); | |
129 | 9 | const char *dte_home = getenv("DTE_HOME"); | |
130 |
1/2✗ Branch 0 (8→9) not taken.
✓ Branch 1 (8→10) taken 9 times.
|
9 | home = home ? home : ""; |
131 | 9 | e->home_dir = strview_intern(home); | |
132 |
2/2✓ Branch 0 (11→12) taken 1 times.
✓ Branch 1 (11→14) taken 8 times.
|
9 | if (dte_home) { |
133 | 1 | e->user_config_dir = xstrdup(dte_home); | |
134 | } else { | ||
135 | 8 | e->user_config_dir = xstrjoin(home, "/.dte"); | |
136 | } | ||
137 | |||
138 | 9 | pid_t pid = getpid(); | |
139 | 9 | bool leader = pid == getsid(0); | |
140 | 9 | e->session_leader = leader; | |
141 |
1/2✓ Branch 0 (18→19) taken 9 times.
✗ Branch 1 (18→20) not taken.
|
18 | LOG_INFO("pid: %jd%s", (intmax_t)pid, leader ? " (session leader)" : ""); |
142 | |||
143 | 9 | pid_t pgid = getpgrp(); | |
144 |
1/2✓ Branch 0 (22→23) taken 9 times.
✗ Branch 1 (22→24) not taken.
|
9 | if (pgid != pid) { |
145 | 9 | LOG_INFO("pgid: %jd", (intmax_t)pgid); | |
146 | } | ||
147 | |||
148 | 9 | mode_t mask = get_umask(); | |
149 | 9 | e->new_file_mode = 0666 & ~mask; | |
150 | 9 | LOG_INFO("umask: %04o", 0777u & (unsigned int)mask); | |
151 | |||
152 | 9 | set_and_check_locale(); | |
153 | 9 | init_file_locks_context(&e->locks_ctx, e->user_config_dir, pid); | |
154 | |||
155 | // Allow child processes to detect that they're running under dte | ||
156 |
1/2✗ Branch 0 (29→30) not taken.
✓ Branch 1 (29→31) taken 9 times.
|
9 | if (unlikely(setenv("DTE_VERSION", VERSION, 1) != 0)) { |
157 | − | fatal_error("setenv", errno); | |
158 | } | ||
159 | |||
160 | 9 | RegexpWordBoundaryTokens *wb = &e->regexp_word_tokens; | |
161 |
1/2✓ Branch 0 (32→33) taken 9 times.
✗ Branch 1 (32→34) not taken.
|
9 | if (regexp_init_word_boundary_tokens(wb)) { |
162 | 9 | LOG_INFO("regex word boundary tokens detected: %s %s", wb->start, wb->end); | |
163 | } else { | ||
164 | ✗ | LOG_WARNING("no regex word boundary tokens detected"); | |
165 | } | ||
166 | |||
167 | 9 | hashmap_init(&e->aliases, 32, HMAP_NO_FLAGS); | |
168 | 9 | hashset_init(&e->required_syntax_files, 0, false); | |
169 | 9 | hashset_init(&e->required_syntax_builtins, 0, false); | |
170 | |||
171 | 9 | HashMap *modes = &e->modes; | |
172 | 9 | e->normal_mode = new_mode(modes, xstrdup("normal"), &normal_commands); | |
173 | 9 | e->command_mode = new_mode(modes, xstrdup("command"), &cmd_mode_commands); | |
174 | 9 | e->search_mode = new_mode(modes, xstrdup("search"), &search_mode_commands); | |
175 | 9 | e->mode = e->normal_mode; | |
176 | 9 | intmap_init(&e->normal_mode->key_bindings, 150); | |
177 | 9 | intmap_init(&e->command_mode->key_bindings, 40); | |
178 | 9 | intmap_init(&e->search_mode->key_bindings, 40); | |
179 | |||
180 | 9 | return e; | |
181 | } | ||
182 | |||
183 | 28 | static void free_mode_handler(ModeHandler *handler) | |
184 | { | ||
185 | 28 | ptr_array_free_array(&handler->fallthrough_modes); | |
186 | 28 | free_bindings(&handler->key_bindings); | |
187 | 28 | free(handler); | |
188 | 28 | } | |
189 | |||
190 | 10 | void clear_all_messages(EditorState *e) | |
191 | { | ||
192 |
2/2✓ Branch 0 (5→3) taken 30 times.
✓ Branch 1 (5→6) taken 10 times.
|
40 | for (size_t i = 0; i < ARRAYLEN(e->messages); i++) { |
193 | 30 | clear_messages(&e->messages[i]); | |
194 | } | ||
195 | 10 | } | |
196 | |||
197 | 9 | void free_editor_state(EditorState *e) | |
198 | { | ||
199 | 9 | free(e->clipboard.buf); | |
200 | 9 | free_file_options(&e->file_options); | |
201 | 9 | free_filetypes(&e->filetypes); | |
202 | 9 | free_syntaxes(&e->syntaxes); | |
203 | 9 | file_history_free(&e->file_history); | |
204 | 9 | history_free(&e->command_history); | |
205 | 9 | history_free(&e->search_history); | |
206 | 9 | search_free_regexp(&e->search); | |
207 | 9 | clear_all_messages(e); | |
208 | 9 | cmdline_free(&e->cmdline); | |
209 | 9 | free_macro(&e->macro); | |
210 | 9 | tag_file_free(&e->tagfile); | |
211 | 9 | free_buffers(&e->buffers, &e->err, &e->locks_ctx); | |
212 | 9 | free_file_locks_context(&e->locks_ctx); | |
213 | |||
214 | 9 | ptr_array_free_cb(&e->bookmarks, FREE_FUNC(file_location_free)); | |
215 | 9 | hashmap_free(&e->compilers, FREE_FUNC(free_compiler)); | |
216 | 9 | hashmap_free(&e->modes, FREE_FUNC(free_mode_handler)); | |
217 | 9 | hashmap_free(&e->styles.other, free); | |
218 | 9 | hashmap_free(&e->aliases, free); | |
219 | 9 | hashset_free(&e->required_syntax_files); | |
220 | 9 | hashset_free(&e->required_syntax_builtins); | |
221 | |||
222 | 9 | free_interned_strings(); | |
223 | 9 | free_interned_regexps(); | |
224 | |||
225 | // TODO: intern this (so that it's freed by free_interned_strings()) | ||
226 | 9 | free((void*)e->user_config_dir); | |
227 | |||
228 | 9 | free(e); | |
229 | 9 | } | |
230 | |||
231 | ✗ | static bool buffer_contains_block(const Buffer *buffer, const Block *ref) | |
232 | { | ||
233 | ✗ | const Block *blk; | |
234 | ✗ | block_for_each(blk, &buffer->blocks) { | |
235 | ✗ | if (blk == ref) { | |
236 | return true; | ||
237 | } | ||
238 | } | ||
239 | return false; | ||
240 | } | ||
241 | |||
242 | ✗ | static void sanity_check(const View *view) | |
243 | { | ||
244 | ✗ | if (!DEBUG_ASSERTIONS_ENABLED) { | |
245 | return; | ||
246 | } | ||
247 | ✗ | const BlockIter *cursor = &view->cursor; | |
248 | ✗ | BUG_ON(!buffer_contains_block(view->buffer, cursor->blk)); | |
249 | ✗ | BUG_ON(cursor->offset > cursor->blk->size); | |
250 | } | ||
251 | |||
252 | ✗ | void any_key(Terminal *term, unsigned int esc_timeout) | |
253 | { | ||
254 | ✗ | KeyCode key; | |
255 | ✗ | xfputs("Press any key to continue\r\n", stderr); | |
256 | ✗ | while ((key = term_read_input(term, esc_timeout)) == KEY_NONE) { | |
257 | ✗ | ; | |
258 | } | ||
259 | ✗ | bool bracketed_paste = key == KEYCODE_BRACKETED_PASTE; | |
260 | ✗ | if (bracketed_paste || key == KEYCODE_DETECTED_PASTE) { | |
261 | ✗ | term_discard_paste(&term->ibuf, bracketed_paste); | |
262 | } | ||
263 | ✗ | } | |
264 | |||
265 | NOINLINE | ||
266 | ✗ | void ui_resize(EditorState *e) | |
267 | { | ||
268 | ✗ | BUG_ON(e->flags & EFLAG_HEADLESS); | |
269 | ✗ | resized = 0; | |
270 | ✗ | update_screen_size(&e->terminal, e->root_frame); | |
271 | |||
272 | ✗ | const ScreenState dummyval = {.id = 0}; | |
273 | ✗ | e->screen_update |= UPDATE_ALL; | |
274 | ✗ | update_screen(e, &dummyval); | |
275 | ✗ | } | |
276 | |||
277 | ✗ | void ui_start(EditorState *e) | |
278 | { | ||
279 | ✗ | BUG_ON(e->flags & EFLAG_HEADLESS); | |
280 | ✗ | Terminal *term = &e->terminal; | |
281 | |||
282 | // Note: the order of these calls is important - Kitty saves/restores | ||
283 | // some terminal state when switching buffers, so switching to the | ||
284 | // alternate screen buffer needs to happen before modes are enabled | ||
285 | ✗ | term_use_alt_screen_buffer(term); | |
286 | ✗ | term_enable_private_modes(term); | |
287 | |||
288 | ✗ | term_restore_and_save_title(term); | |
289 | ✗ | ui_resize(e); | |
290 | ✗ | } | |
291 | |||
292 | // Like ui_start(), but to be called only the first time the UI is started. | ||
293 | // Terminal queries are buffered before ui_resize() is called, so that only | ||
294 | // a single term_output_flush() is needed (i.e. as opposed to calling | ||
295 | // ui_start() + term_put_initial_queries() + term_output_flush()). | ||
296 | ✗ | void ui_first_start(EditorState *e, unsigned int terminal_query_level) | |
297 | { | ||
298 | ✗ | BUG_ON(e->flags & EFLAG_HEADLESS); | |
299 | ✗ | Terminal *term = &e->terminal; | |
300 | |||
301 | // The order of these calls is important; see ui_start() | ||
302 | ✗ | term_use_alt_screen_buffer(term); | |
303 | ✗ | term_enable_private_modes(term); | |
304 | |||
305 | ✗ | term_save_title(term); | |
306 | ✗ | term_put_initial_queries(term, terminal_query_level); | |
307 | ✗ | ui_resize(e); | |
308 | ✗ | } | |
309 | |||
310 | ✗ | void ui_end(EditorState *e, bool final) | |
311 | { | ||
312 | ✗ | BUG_ON(e->flags & EFLAG_HEADLESS); | |
313 | ✗ | Terminal *term = &e->terminal; | |
314 | ✗ | TermOutputBuffer *obuf = &term->obuf; | |
315 | |||
316 | ✗ | if (final) { | |
317 | ✗ | term_restore_title(term); | |
318 | } else { | ||
319 | ✗ | term_restore_and_save_title(term); | |
320 | } | ||
321 | |||
322 | ✗ | term_clear_screen(obuf); | |
323 | ✗ | term_move_cursor(obuf, 0, term->height - 1); | |
324 | ✗ | term_restore_cursor_style(term); | |
325 | ✗ | term_show_cursor(term); | |
326 | ✗ | term_restore_private_modes(term); | |
327 | ✗ | term_use_normal_screen_buffer(term); | |
328 | ✗ | term_end_sync_update(term); | |
329 | ✗ | term_output_flush(obuf); | |
330 | ✗ | } | |
331 | |||
332 | ✗ | int main_loop(EditorState *e) | |
333 | { | ||
334 | ✗ | BUG_ON(e->flags & EFLAG_HEADLESS); | |
335 | |||
336 | ✗ | while (e->status == EDITOR_RUNNING) { | |
337 | ✗ | if (unlikely(resized)) { | |
338 | ✗ | LOG_INFO("SIGWINCH received"); | |
339 | ✗ | ui_resize(e); | |
340 | } | ||
341 | |||
342 | ✗ | KeyCode key = term_read_input(&e->terminal, e->options.esc_timeout); | |
343 | ✗ | if (unlikely(key == KEY_NONE)) { | |
344 | ✗ | continue; | |
345 | } | ||
346 | |||
347 | ✗ | const ScreenState s = get_screen_state(e->view); | |
348 | ✗ | clear_error(&e->err); | |
349 | ✗ | handle_input(e, key); | |
350 | ✗ | sanity_check(e->view); | |
351 | ✗ | update_screen(e, &s); | |
352 | } | ||
353 | |||
354 | ✗ | BUG_ON(e->status < 0 || e->status > EDITOR_EXIT_MAX); | |
355 | ✗ | return e->status; | |
356 | } | ||
357 |