diff options
Diffstat (limited to 'examples/dte/main.c')
| -rw-r--r-- | examples/dte/main.c | 575 |
1 files changed, 575 insertions, 0 deletions
diff --git a/examples/dte/main.c b/examples/dte/main.c new file mode 100644 index 0000000..11025af --- /dev/null +++ b/examples/dte/main.c @@ -0,0 +1,575 @@ +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/utsname.h> +#include <unistd.h> +#include "block.h" +#include "commands.h" +#include "compiler.h" +#include "config.h" +#include "editor.h" +#include "error.h" +#include "file-history.h" +#include "frame.h" +#include "history.h" +#include "load-save.h" +#include "move.h" +#include "screen.h" +#include "search.h" +#include "signals.h" +#include "syntax/state.h" +#include "syntax/syntax.h" +#include "tag.h" +#include "terminal/input.h" +#include "terminal/key.h" +#include "terminal/mode.h" +#include "terminal/output.h" +#include "terminal/terminal.h" +#include "util/debug.h" +#include "util/exitcode.h" +#include "util/fd.h" +#include "util/log.h" +#include "util/macros.h" +#include "util/path.h" +#include "util/ptr-array.h" +#include "util/strtonum.h" +#include "util/xmalloc.h" +#include "util/xreadwrite.h" +#include "util/xsnprintf.h" +#include "view.h" +#include "window.h" +#include "../build/version.h" + +static void term_cleanup(EditorState *e) +{ + set_fatal_error_cleanup_handler(NULL, NULL); + if (!e->child_controls_terminal) { + ui_end(e); + } +} + +static void cleanup_handler(void *userdata) +{ + term_cleanup(userdata); +} + +static ExitCode write_stdout(const char *str, size_t len) +{ + if (xwrite_all(STDOUT_FILENO, str, len) < 0) { + perror("write"); + return EX_IOERR; + } + return EX_OK; +} + +static ExitCode list_builtin_configs(void) +{ + String str = dump_builtin_configs(); + BUG_ON(!str.buffer); + ExitCode e = write_stdout(str.buffer, str.len); + string_free(&str); + return e; +} + +static ExitCode dump_builtin_config(const char *name) +{ + const BuiltinConfig *cfg = get_builtin_config(name); + if (!cfg) { + fprintf(stderr, "Error: no built-in config with name '%s'\n", name); + return EX_USAGE; + } + return write_stdout(cfg->text.data, cfg->text.length); +} + +static ExitCode lint_syntax(const char *filename) +{ + EditorState *e = init_editor_state(); + int err; + BUG_ON(e->status != EDITOR_INITIALIZING); + const Syntax *s = load_syntax_file(e, filename, CFG_MUST_EXIST, &err); + if (s) { + const size_t n = s->states.count; + const char *plural = (n == 1) ? "" : "s"; + printf("OK: loaded syntax '%s' with %zu state%s\n", s->name, n, plural); + } else if (err == EINVAL) { + error_msg("%s: no default syntax found", filename); + } + free_editor_state(e); + return get_nr_errors() ? EX_DATAERR : EX_OK; +} + +static ExitCode showkey_loop(const char *term_name, const char *colorterm) +{ + if (unlikely(!term_raw())) { + perror("tcsetattr"); + return EX_IOERR; + } + + Terminal term; + TermOutputBuffer *obuf = &term.obuf; + TermInputBuffer *ibuf = &term.ibuf; + term_init(&term, term_name, colorterm); + term_input_init(ibuf); + term_output_init(obuf); + term_enable_private_modes(&term); + term_add_literal(obuf, "Press any key combination, or use Ctrl+D to exit\r\n"); + term_output_flush(obuf); + + char keystr[KEYCODE_STR_MAX]; + for (bool loop = true; loop; ) { + KeyCode key = term_read_key(&term, 100); + switch (key) { + case KEY_NONE: + case KEY_IGNORE: + continue; + case KEY_BRACKETED_PASTE: + case KEY_DETECTED_PASTE: + term_discard_paste(ibuf, key == KEY_BRACKETED_PASTE); + continue; + case MOD_CTRL | 'd': + loop = false; + } + size_t keylen = keycode_to_string(key, keystr); + term_add_literal(obuf, " "); + term_add_bytes(obuf, keystr, keylen); + term_add_literal(obuf, "\r\n"); + term_output_flush(obuf); + } + + term_restore_private_modes(&term); + term_output_flush(obuf); + term_cooked(); + term_input_free(ibuf); + term_output_free(obuf); + return EX_OK; +} + +static ExitCode init_std_fds(int std_fds[2]) +{ + FILE *streams[3] = {stdin, stdout, stderr}; + for (int i = 0; i < ARRAYLEN(streams); i++) { + if (is_controlling_tty(i)) { + continue; + } + + if (i < STDERR_FILENO) { + // Try to create a duplicate fd for redirected stdin/stdout; to + // allow reading/writing after freopen(3) closes the original + int fd = fcntl(i, F_DUPFD_CLOEXEC, 3); + if (fd == -1 && errno != EBADF) { + perror("fcntl"); + return EX_OSERR; + } + std_fds[i] = fd; + } + + // Ensure standard streams are connected to the terminal during + // editor operation, regardless of how they were redirected + if (unlikely(!freopen("/dev/tty", i ? "w" : "r", streams[i]))) { + const char *err = strerror(errno); + fprintf(stderr, "Failed to open tty for fd %d: %s\n", i, err); + return EX_IOERR; + } + + int new_fd = fileno(streams[i]); + if (unlikely(new_fd != i)) { + // This should never happen in a single-threaded program. + // freopen() should call fclose() followed by open() and + // POSIX requires a successful call to open() to return the + // lowest available file descriptor. + fprintf(stderr, "freopen() changed fd from %d to %d\n", i, new_fd); + return EX_OSERR; + } + + if (unlikely(!is_controlling_tty(new_fd))) { + perror("tcgetpgrp"); + return EX_OSERR; + } + } + + return EX_OK; +} + +static Buffer *init_std_buffer(EditorState *e, int fds[2]) +{ + const char *name = NULL; + Buffer *buffer = NULL; + + if (fds[STDIN_FILENO] >= 3) { + Encoding enc = encoding_from_type(UTF8); + buffer = buffer_new(&e->buffers, &e->options, &enc); + if (read_blocks(buffer, fds[STDIN_FILENO], false)) { + name = "(stdin)"; + buffer->temporary = true; + } else { + error_msg("Unable to read redirected stdin"); + remove_and_free_buffer(&e->buffers, buffer); + buffer = NULL; + } + } + + if (fds[STDOUT_FILENO] >= 3) { + if (!buffer) { + buffer = open_empty_buffer(&e->buffers, &e->options); + name = "(stdout)"; + } else { + name = "(stdin|stdout)"; + } + buffer->stdout_buffer = true; + buffer->temporary = true; + } + + BUG_ON(!buffer != !name); + if (name) { + set_display_filename(buffer, xstrdup(name)); + } + + return buffer; +} + +static ExitCode init_logging(const char *filename, const char *req_level_str) +{ + if (!filename || filename[0] == '\0') { + return EX_OK; + } + + LogLevel req_level = log_level_from_str(req_level_str); + if (req_level == LOG_LEVEL_NONE) { + return EX_OK; + } + if (req_level == LOG_LEVEL_INVALID) { + fprintf(stderr, "Invalid $DTE_LOG_LEVEL value: '%s'\n", req_level_str); + return EX_USAGE; + } + + // https://no-color.org/ + const char *no_color = xgetenv("NO_COLOR"); + + LogLevel got_level = log_open(filename, req_level, !no_color); + if (got_level == LOG_LEVEL_NONE) { + const char *err = strerror(errno); + fprintf(stderr, "Failed to open $DTE_LOG (%s): %s\n", filename, err); + return EX_IOERR; + } + + const char *got_level_str = log_level_to_str(got_level); + if (got_level != req_level) { + const char *r = req_level_str; + const char *g = got_level_str; + LOG_WARNING("log level '%s' unavailable; falling back to '%s'", r, g); + } + + LOG_INFO("logging to '%s' (level: %s)", filename, got_level_str); + + if (no_color) { + LOG_INFO("log colors disabled ($NO_COLOR)"); + } + + struct utsname u; + if (likely(uname(&u) >= 0)) { + LOG_INFO("system: %s/%s %s", u.sysname, u.machine, u.release); + } else { + LOG_ERRNO("uname"); + } + return EX_OK; +} + +static void log_config_counts(const EditorState *e) +{ + if (!log_level_enabled(LOG_LEVEL_INFO)) { + return; + } + + size_t nbinds = 0; + for (size_t i = 0; i < ARRAYLEN(e->modes); i++) { + nbinds += e->modes[i].key_bindings.count; + } + + size_t nerrorfmts = 0; + for (HashMapIter it = hashmap_iter(&e->compilers); hashmap_next(&it); ) { + const Compiler *compiler = it.entry->value; + nerrorfmts += compiler->error_formats.count; + } + + LOG_INFO ( + "binds=%zu aliases=%zu hi=%zu ft=%zu option=%zu errorfmt=%zu(%zu)", + nbinds, + e->aliases.count, + e->colors.other.count + NR_BC, + e->filetypes.count, + e->file_options.count, + e->compilers.count, + nerrorfmts + ); +} + +static const char copyright[] = + "dte " VERSION "\n" + "(C) 2013-2023 Craig Barnes\n" + "(C) 2010-2015 Timo Hirvonen\n" + "This program is free software; you can redistribute and/or modify\n" + "it under the terms of the GNU General Public License version 2\n" + "<https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.\n" + "There is NO WARRANTY, to the extent permitted by law.\n"; + +static const char usage[] = + "Usage: %s [OPTIONS] [[+LINE] FILE]...\n\n" + "Options:\n" + " -c COMMAND Run COMMAND after editor starts\n" + " -t CTAG Jump to source location of CTAG\n" + " -r RCFILE Read user config from RCFILE instead of ~/.dte/rc\n" + " -s FILE Validate dte-syntax commands in FILE and exit\n" + " -b NAME Print built-in config matching NAME and exit\n" + " -B Print list of built-in config names and exit\n" + " -H Don't load or save history files\n" + " -R Don't read user config file\n" + " -K Start editor in \"showkey\" mode\n" + " -h Display help summary and exit\n" + " -V Display version number and exit\n" + "\n"; + +int main(int argc, char *argv[]) +{ + static const char optstring[] = "hBHKRVb:c:t:r:s:"; + const char *tag = NULL; + const char *rc = NULL; + const char *commands[8]; + size_t nr_commands = 0; + bool read_rc = true; + bool use_showkey = false; + bool load_and_save_history = true; + set_print_errors_to_stderr(true); + + for (int ch; (ch = getopt(argc, argv, optstring)) != -1; ) { + switch (ch) { + case 'c': + if (unlikely(nr_commands >= ARRAYLEN(commands))) { + fputs("Error: too many -c options used\n", stderr); + return EX_USAGE; + } + commands[nr_commands++] = optarg; + break; + case 't': + tag = optarg; + break; + case 'r': + rc = optarg; + break; + case 's': + return lint_syntax(optarg); + case 'R': + read_rc = false; + break; + case 'b': + return dump_builtin_config(optarg); + case 'B': + return list_builtin_configs(); + case 'H': + load_and_save_history = false; + break; + case 'K': + use_showkey = true; + goto loop_break; + case 'V': + return write_stdout(copyright, sizeof(copyright)); + case 'h': + printf(usage, (argv[0] && argv[0][0]) ? argv[0] : "dte"); + return EX_OK; + default: + return EX_USAGE; + } + } + +loop_break:; + + const char *term_name = xgetenv("TERM"); + if (!term_name) { + fputs("Error: $TERM not set\n", stderr); + // This is considered a "usage" error, because the program + // must be started from a properly configured terminal + return EX_USAGE; + } + + // This must be done before calling init_logging(), otherwise an + // invocation like e.g. `DTE_LOG=/dev/pts/2 dte 0<&-` could + // cause the logging fd to be opened as STDIN_FILENO + int std_fds[2] = {-1, -1}; + ExitCode r = init_std_fds(std_fds); + if (unlikely(r != EX_OK)) { + return r; + } + + r = init_logging(getenv("DTE_LOG"), getenv("DTE_LOG_LEVEL")); + if (unlikely(r != EX_OK)) { + return r; + } + + if (!term_mode_init()) { + perror("tcgetattr"); + return EX_IOERR; + } + + const char *colorterm = getenv("COLORTERM"); + if (use_showkey) { + return showkey_loop(term_name, colorterm); + } + + EditorState *e = init_editor_state(); + Terminal *term = &e->terminal; + term_init(term, term_name, colorterm); + + Buffer *std_buffer = init_std_buffer(e, std_fds); + bool have_stdout_buffer = std_buffer && std_buffer->stdout_buffer; + + // Create this early (needed if "lock-files" is true) + const char *cfgdir = e->user_config_dir; + BUG_ON(!cfgdir); + if (mkdir(cfgdir, 0755) != 0 && errno != EEXIST) { + error_msg("Error creating %s: %s", cfgdir, strerror(errno)); + load_and_save_history = false; + e->options.lock_files = false; + } + + term_save_title(term); + exec_builtin_rc(e); + + if (read_rc) { + ConfigFlags flags = CFG_NOFLAGS; + char buf[4096]; + if (rc) { + flags |= CFG_MUST_EXIST; + } else { + xsnprintf(buf, sizeof buf, "%s/%s", cfgdir, "rc"); + rc = buf; + } + LOG_INFO("loading configuration from %s", rc); + read_normal_config(e, rc, flags); + } + + log_config_counts(e); + update_all_syntax_colors(&e->syntaxes, &e->colors); + + Window *window = new_window(e); + e->window = window; + e->root_frame = new_root_frame(window); + + set_signal_handlers(); + set_fatal_error_cleanup_handler(cleanup_handler, e); + + if (load_and_save_history) { + file_history_load(&e->file_history, path_join(cfgdir, "file-history")); + history_load(&e->command_history, path_join(cfgdir, "command-history")); + history_load(&e->search_history, path_join(cfgdir, "search-history")); + if (e->search_history.last) { + search_set_regexp(&e->search, e->search_history.last->text); + } + } + + set_print_errors_to_stderr(false); + + // Initialize terminal but don't update screen yet. Also display + // "Press any key to continue" prompt if there were any errors + // during reading configuration files. + if (!term_raw()) { + perror("tcsetattr"); + return EX_IOERR; + } + if (get_nr_errors()) { + any_key(term, e->options.esc_timeout); + clear_error(); + } + + e->status = EDITOR_RUNNING; + + for (size_t i = optind, line = 0, col = 0; i < argc; i++) { + const char *str = argv[i]; + if (line == 0 && *str == '+' && str_to_filepos(str + 1, &line, &col)) { + continue; + } + View *view = window_open_buffer(window, str, false, NULL); + if (line == 0) { + continue; + } + set_view(view); + move_to_filepos(view, line, col); + line = 0; + } + + if (std_buffer) { + window_add_buffer(window, std_buffer); + } + + View *dview = NULL; + if (window->views.count == 0) { + // Open a default buffer, if none were opened for arguments + dview = window_open_empty_buffer(window); + BUG_ON(!dview); + BUG_ON(window->views.count != 1); + BUG_ON(dview != window->views.ptrs[0]); + } + + set_view(window->views.ptrs[0]); + ui_start(e); + + for (size_t i = 0; i < nr_commands; i++) { + handle_normal_command(e, commands[i], false); + } + + if (tag) { + StringView tag_sv = strview_from_cstring(tag); + if (tag_lookup(&e->tagfile, &tag_sv, NULL, &e->messages)) { + activate_current_message(e); + if (dview && nr_commands == 0 && window->views.count > 1) { + // Close default/empty buffer, if `-t` jumped to a tag + // and no commands were executed via `-c` + remove_view(dview); + dview = NULL; + } + } + } + + if (nr_commands > 0 || tag) { + normal_update(e); + } + + int exit_code = main_loop(e); + + term_restore_title(term); + ui_end(e); + term_output_flush(&term->obuf); + set_print_errors_to_stderr(true); + + // Unlock files and add to file history + remove_frame(e, e->root_frame); + + if (load_and_save_history) { + history_save(&e->command_history); + history_save(&e->search_history); + file_history_save(&e->file_history); + } + + if (have_stdout_buffer) { + int fd = std_fds[STDOUT_FILENO]; + Block *blk; + block_for_each(blk, &std_buffer->blocks) { + if (xwrite_all(fd, blk->data, blk->size) < 0) { + error_msg_errno("failed to write (stdout) buffer"); + if (exit_code == EDITOR_EXIT_OK) { + exit_code = EX_IOERR; + } + break; + } + } + free_blocks(std_buffer); + free(std_buffer); + } + + free_editor_state(e); + LOG_INFO("exiting with status %d", exit_code); + log_close(); + return exit_code; +} |
