diff options
Diffstat (limited to 'examples/dte/editor.c')
| -rw-r--r-- | examples/dte/editor.c | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/examples/dte/editor.c b/examples/dte/editor.c new file mode 100644 index 0000000..aa88d6e --- /dev/null +++ b/examples/dte/editor.c | |||
| @@ -0,0 +1,321 @@ | |||
| 1 | #include "compat.h" | ||
| 2 | #include <errno.h> | ||
| 3 | #include <langinfo.h> | ||
| 4 | #include <locale.h> | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stdio.h> | ||
| 7 | #include <stdlib.h> | ||
| 8 | #include <string.h> | ||
| 9 | #include <unistd.h> | ||
| 10 | #include "editor.h" | ||
| 11 | #include "bind.h" | ||
| 12 | #include "bookmark.h" | ||
| 13 | #include "command/macro.h" | ||
| 14 | #include "commands.h" | ||
| 15 | #include "compiler.h" | ||
| 16 | #include "encoding.h" | ||
| 17 | #include "error.h" | ||
| 18 | #include "file-option.h" | ||
| 19 | #include "filetype.h" | ||
| 20 | #include "lock.h" | ||
| 21 | #include "mode.h" | ||
| 22 | #include "regexp.h" | ||
| 23 | #include "screen.h" | ||
| 24 | #include "search.h" | ||
| 25 | #include "signals.h" | ||
| 26 | #include "syntax/syntax.h" | ||
| 27 | #include "tag.h" | ||
| 28 | #include "terminal/input.h" | ||
| 29 | #include "terminal/mode.h" | ||
| 30 | #include "terminal/output.h" | ||
| 31 | #include "terminal/style.h" | ||
| 32 | #include "util/ascii.h" | ||
| 33 | #include "util/debug.h" | ||
| 34 | #include "util/exitcode.h" | ||
| 35 | #include "util/intern.h" | ||
| 36 | #include "util/log.h" | ||
| 37 | #include "util/utf8.h" | ||
| 38 | #include "util/xmalloc.h" | ||
| 39 | #include "util/xstdio.h" | ||
| 40 | #include "window.h" | ||
| 41 | #include "../build/version.h" | ||
| 42 | |||
| 43 | static void set_and_check_locale(void) | ||
| 44 | { | ||
| 45 | const char *default_locale = setlocale(LC_CTYPE, ""); | ||
| 46 | if (likely(default_locale)) { | ||
| 47 | const char *codeset = nl_langinfo(CODESET); | ||
| 48 | LOG_INFO("locale: %s (codeset: %s)", default_locale, codeset); | ||
| 49 | if (likely(lookup_encoding(codeset) == UTF8)) { | ||
| 50 | return; | ||
| 51 | } | ||
| 52 | } else { | ||
| 53 | LOG_ERROR("failed to set default locale"); | ||
| 54 | } | ||
| 55 | |||
| 56 | static const char fallbacks[][12] = {"C.UTF-8", "en_US.UTF-8"}; | ||
| 57 | const char *fallback = NULL; | ||
| 58 | for (size_t i = 0; i < ARRAYLEN(fallbacks) && !fallback; i++) { | ||
| 59 | fallback = setlocale(LC_CTYPE, fallbacks[i]); | ||
| 60 | } | ||
| 61 | if (fallback) { | ||
| 62 | LOG_INFO("using fallback locale for LC_CTYPE: %s", fallback); | ||
| 63 | return; | ||
| 64 | } | ||
| 65 | |||
| 66 | LOG_ERROR("no UTF-8 fallback locales found"); | ||
| 67 | fputs("setlocale() failed\n", stderr); | ||
| 68 | exit(EX_CONFIG); | ||
| 69 | } | ||
| 70 | |||
| 71 | EditorState *init_editor_state(void) | ||
| 72 | { | ||
| 73 | EditorState *e = xnew(EditorState, 1); | ||
| 74 | *e = (EditorState) { | ||
| 75 | .status = EDITOR_INITIALIZING, | ||
| 76 | .input_mode = INPUT_NORMAL, | ||
| 77 | .version = VERSION, | ||
| 78 | .command_history = { | ||
| 79 | .max_entries = 512, | ||
| 80 | }, | ||
| 81 | .search_history = { | ||
| 82 | .max_entries = 128, | ||
| 83 | }, | ||
| 84 | .cursor_styles = { | ||
| 85 | [CURSOR_MODE_DEFAULT] = {.type = CURSOR_DEFAULT, .color = COLOR_DEFAULT}, | ||
| 86 | [CURSOR_MODE_INSERT] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
| 87 | [CURSOR_MODE_OVERWRITE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
| 88 | [CURSOR_MODE_CMDLINE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
| 89 | }, | ||
| 90 | .modes = { | ||
| 91 | [INPUT_NORMAL] = {.cmds = &normal_commands}, | ||
| 92 | [INPUT_COMMAND] = {.cmds = &cmd_mode_commands}, | ||
| 93 | [INPUT_SEARCH] = {.cmds = &search_mode_commands}, | ||
| 94 | }, | ||
| 95 | .options = { | ||
| 96 | .auto_indent = true, | ||
| 97 | .detect_indent = 0, | ||
| 98 | .editorconfig = false, | ||
| 99 | .emulate_tab = false, | ||
| 100 | .expand_tab = false, | ||
| 101 | .file_history = true, | ||
| 102 | .indent_width = 8, | ||
| 103 | .overwrite = false, | ||
| 104 | .save_unmodified = SAVE_FULL, | ||
| 105 | .syntax = true, | ||
| 106 | .tab_width = 8, | ||
| 107 | .text_width = 72, | ||
| 108 | .ws_error = WSE_SPECIAL, | ||
| 109 | |||
| 110 | // Global-only options | ||
| 111 | .case_sensitive_search = CSS_TRUE, | ||
| 112 | .crlf_newlines = false, | ||
| 113 | .display_special = false, | ||
| 114 | .esc_timeout = 100, | ||
| 115 | .filesize_limit = 250, | ||
| 116 | .lock_files = true, | ||
| 117 | .optimize_true_color = true, | ||
| 118 | .scroll_margin = 0, | ||
| 119 | .select_cursor_char = true, | ||
| 120 | .set_window_title = false, | ||
| 121 | .show_line_numbers = false, | ||
| 122 | .statusline_left = str_intern(" %f%s%m%s%r%s%M"), | ||
| 123 | .statusline_right = str_intern(" %y,%X %u %o %E%s%b%s%n %t %p "), | ||
| 124 | .tab_bar = true, | ||
| 125 | .utf8_bom = false, | ||
| 126 | } | ||
| 127 | }; | ||
| 128 | |||
| 129 | sanity_check_global_options(&e->options); | ||
| 130 | |||
| 131 | for (size_t i = 0; i < ARRAYLEN(e->modes); i++) { | ||
| 132 | const CommandSet *cmds = e->modes[i].cmds; | ||
| 133 | BUG_ON(!cmds); | ||
| 134 | BUG_ON(!cmds->lookup); | ||
| 135 | } | ||
| 136 | |||
| 137 | const char *home = getenv("HOME"); | ||
| 138 | const char *dte_home = getenv("DTE_HOME"); | ||
| 139 | e->home_dir = strview_intern(home ? home : ""); | ||
| 140 | if (dte_home) { | ||
| 141 | e->user_config_dir = xstrdup(dte_home); | ||
| 142 | } else { | ||
| 143 | e->user_config_dir = xasprintf("%s/.dte", e->home_dir.data); | ||
| 144 | } | ||
| 145 | |||
| 146 | LOG_INFO("dte version: " VERSION); | ||
| 147 | LOG_INFO("features:%s", feature_string); | ||
| 148 | |||
| 149 | pid_t pid = getpid(); | ||
| 150 | bool leader = pid == getsid(0); | ||
| 151 | e->session_leader = leader; | ||
| 152 | LOG_INFO("pid: %jd%s", (intmax_t)pid, leader ? " (session leader)" : ""); | ||
| 153 | |||
| 154 | pid_t pgid = getpgrp(); | ||
| 155 | if (pgid != pid) { | ||
| 156 | LOG_INFO("pgid: %jd", (intmax_t)pgid); | ||
| 157 | } | ||
| 158 | |||
| 159 | set_and_check_locale(); | ||
| 160 | init_file_locks_context(e->user_config_dir, pid); | ||
| 161 | |||
| 162 | // Allow child processes to detect that they're running under dte | ||
| 163 | if (unlikely(setenv("DTE_VERSION", VERSION, true) != 0)) { | ||
| 164 | fatal_error("setenv", errno); | ||
| 165 | } | ||
| 166 | |||
| 167 | RegexpWordBoundaryTokens *wb = &e->regexp_word_tokens; | ||
| 168 | if (regexp_init_word_boundary_tokens(wb)) { | ||
| 169 | LOG_INFO("regex word boundary tokens detected: %s %s", wb->start, wb->end); | ||
| 170 | } else { | ||
| 171 | LOG_WARNING("no regex word boundary tokens detected"); | ||
| 172 | } | ||
| 173 | |||
| 174 | term_input_init(&e->terminal.ibuf); | ||
| 175 | term_output_init(&e->terminal.obuf); | ||
| 176 | hashmap_init(&e->aliases, 32); | ||
| 177 | intmap_init(&e->modes[INPUT_NORMAL].key_bindings, 150); | ||
| 178 | intmap_init(&e->modes[INPUT_COMMAND].key_bindings, 40); | ||
| 179 | intmap_init(&e->modes[INPUT_SEARCH].key_bindings, 40); | ||
| 180 | return e; | ||
| 181 | } | ||
| 182 | |||
| 183 | void free_editor_state(EditorState *e) | ||
| 184 | { | ||
| 185 | free(e->clipboard.buf); | ||
| 186 | free_file_options(&e->file_options); | ||
| 187 | free_filetypes(&e->filetypes); | ||
| 188 | free_syntaxes(&e->syntaxes); | ||
| 189 | file_history_free(&e->file_history); | ||
| 190 | history_free(&e->command_history); | ||
| 191 | history_free(&e->search_history); | ||
| 192 | search_free_regexp(&e->search); | ||
| 193 | term_output_free(&e->terminal.obuf); | ||
| 194 | term_input_free(&e->terminal.ibuf); | ||
| 195 | cmdline_free(&e->cmdline); | ||
| 196 | clear_messages(&e->messages); | ||
| 197 | free_macro(&e->macro); | ||
| 198 | tag_file_free(&e->tagfile); | ||
| 199 | |||
| 200 | ptr_array_free_cb(&e->bookmarks, FREE_FUNC(file_location_free)); | ||
| 201 | ptr_array_free_cb(&e->buffers, FREE_FUNC(free_buffer)); | ||
| 202 | hashmap_free(&e->compilers, FREE_FUNC(free_compiler)); | ||
| 203 | hashmap_free(&e->colors.other, free); | ||
| 204 | hashmap_free(&e->aliases, free); | ||
| 205 | |||
| 206 | for (size_t i = 0; i < ARRAYLEN(e->modes); i++) { | ||
| 207 | free_bindings(&e->modes[i].key_bindings); | ||
| 208 | } | ||
| 209 | |||
| 210 | free_interned_strings(); | ||
| 211 | free_interned_regexps(); | ||
| 212 | |||
| 213 | // TODO: intern this (so that it's freed by free_intern_pool()) | ||
| 214 | free((void*)e->user_config_dir); | ||
| 215 | |||
| 216 | free(e); | ||
| 217 | } | ||
| 218 | |||
| 219 | static void sanity_check(const View *view) | ||
| 220 | { | ||
| 221 | #if DEBUG >= 1 | ||
| 222 | const Block *blk; | ||
| 223 | block_for_each(blk, &view->buffer->blocks) { | ||
| 224 | if (blk == view->cursor.blk) { | ||
| 225 | BUG_ON(view->cursor.offset > view->cursor.blk->size); | ||
| 226 | return; | ||
| 227 | } | ||
| 228 | } | ||
| 229 | BUG("cursor not seen"); | ||
| 230 | #else | ||
| 231 | (void)view; | ||
| 232 | #endif | ||
| 233 | } | ||
| 234 | |||
| 235 | void any_key(Terminal *term, unsigned int esc_timeout) | ||
| 236 | { | ||
| 237 | KeyCode key; | ||
| 238 | xfputs("Press any key to continue\r\n", stderr); | ||
| 239 | while ((key = term_read_key(term, esc_timeout)) == KEY_NONE) { | ||
| 240 | ; | ||
| 241 | } | ||
| 242 | bool bracketed_paste = key == KEY_BRACKETED_PASTE; | ||
| 243 | if (bracketed_paste || key == KEY_DETECTED_PASTE) { | ||
| 244 | term_discard_paste(&term->ibuf, bracketed_paste); | ||
| 245 | } | ||
| 246 | } | ||
| 247 | |||
| 248 | NOINLINE | ||
| 249 | void ui_resize(EditorState *e) | ||
| 250 | { | ||
| 251 | if (e->status == EDITOR_INITIALIZING) { | ||
| 252 | return; | ||
| 253 | } | ||
| 254 | resized = 0; | ||
| 255 | update_screen_size(&e->terminal, e->root_frame); | ||
| 256 | normal_update(e); | ||
| 257 | } | ||
| 258 | |||
| 259 | void ui_start(EditorState *e) | ||
| 260 | { | ||
| 261 | if (e->status == EDITOR_INITIALIZING) { | ||
| 262 | return; | ||
| 263 | } | ||
| 264 | |||
| 265 | // Note: the order of these calls is important - Kitty saves/restores | ||
| 266 | // some terminal state when switching buffers, so switching to the | ||
| 267 | // alternate screen buffer needs to happen before modes are enabled | ||
| 268 | term_use_alt_screen_buffer(&e->terminal); | ||
| 269 | term_enable_private_modes(&e->terminal); | ||
| 270 | |||
| 271 | ui_resize(e); | ||
| 272 | } | ||
| 273 | |||
| 274 | void ui_end(EditorState *e) | ||
| 275 | { | ||
| 276 | if (e->status == EDITOR_INITIALIZING) { | ||
| 277 | return; | ||
| 278 | } | ||
| 279 | Terminal *term = &e->terminal; | ||
| 280 | TermOutputBuffer *obuf = &term->obuf; | ||
| 281 | term_clear_screen(obuf); | ||
| 282 | term_move_cursor(obuf, 0, term->height - 1); | ||
| 283 | term_restore_cursor_style(term); | ||
| 284 | term_show_cursor(term); | ||
| 285 | term_restore_private_modes(term); | ||
| 286 | term_use_normal_screen_buffer(term); | ||
| 287 | term_end_sync_update(term); | ||
| 288 | term_output_flush(obuf); | ||
| 289 | term_cooked(); | ||
| 290 | } | ||
| 291 | |||
| 292 | int main_loop(EditorState *e) | ||
| 293 | { | ||
| 294 | while (e->status == EDITOR_RUNNING) { | ||
| 295 | if (unlikely(resized)) { | ||
| 296 | LOG_INFO("SIGWINCH received"); | ||
| 297 | ui_resize(e); | ||
| 298 | } | ||
| 299 | |||
| 300 | KeyCode key = term_read_key(&e->terminal, e->options.esc_timeout); | ||
| 301 | if (unlikely(key == KEY_NONE)) { | ||
| 302 | continue; | ||
| 303 | } | ||
| 304 | |||
| 305 | const ScreenState s = { | ||
| 306 | .is_modified = buffer_modified(e->buffer), | ||
| 307 | .id = e->buffer->id, | ||
| 308 | .cy = e->view->cy, | ||
| 309 | .vx = e->view->vx, | ||
| 310 | .vy = e->view->vy | ||
| 311 | }; | ||
| 312 | |||
| 313 | clear_error(); | ||
| 314 | handle_input(e, key); | ||
| 315 | sanity_check(e->view); | ||
| 316 | update_screen(e, &s); | ||
| 317 | } | ||
| 318 | |||
| 319 | BUG_ON(e->status < 0 || e->status > EDITOR_EXIT_MAX); | ||
| 320 | return e->status; | ||
| 321 | } | ||
