#include "assert.hh" #include "backtrace.hh" #include "buffer.hh" #include "buffer_manager.hh" #include "buffer_utils.hh" #include "client_manager.hh" #include "command_manager.hh" #include "commands.hh" #include "context.hh" #include "event_manager.hh" #include "face_registry.hh" #include "file.hh" #include "highlighters.hh" #include "insert_completer.hh" #include "json_ui.hh" #include "ncurses_ui.hh" #include "option_types.hh" #include "parameters_parser.hh" #include "ranges.hh" #include "regex.hh" #include "register_manager.hh" #include "remote.hh" #include "scope.hh" #include "shared_string.hh" #include "shell_manager.hh" #include "string.hh" #include "unit_tests.hh" #include "window.hh" #include "clock.hh" #include #include #include #include #include #include namespace Kakoune { extern const char* version; struct { unsigned int version; StringView notes; } constexpr version_notes[] = { { 0, "» {+u}$kak_command_fifo{} support\n" "» {+u}set-option -remove{} support\n" "» prompts auto select {+i}menu{} completions on space\n" "» explicit completion support ({+b}...{}) in prompts\n" "» {+u}write -atomic{} was replaced with {+u}write -method {}\n" }, { 20200901, "» daemon mode does not fork anymore\n" }, { 20200804, "» {+u}User{} hook support\n" "» Removed {+i}bold{} and {+i}italic{} faces from colorschemes\n" "» Read from stdin support for clients\n" "» {+u}rgba:RRGGBBAA{} faces and alpha blending\n" }, { 20200116, "» {+u}InsertCompletionHide{} parameter is now the list of inserted ranges\n" }, { 20191210, "» {+u}ModeChange{} parameter has changed to contain push/pop\n" "» {+ui}Mode{+u}Begin{}/{+ui}Mode{+u}End{} hooks were removed\n" }, { 20190701, "» {+u}%file\\{}{} expansions to read files\n" "» {+u}echo -to-file {} to write to file\n" "» completions option have an on select command instead of " "a docstring\n" "» Function key syntax do not accept lower case f anymore\n" "» shell quoting of list options is now opt-in with " "{+u}$kak_quoted_...{}\n" }, { 20190120, "» named capture groups in regex\n" "» auto_complete option renamed to autocomplete\n" }, { 20181027, "» {+u}define-commands{} {+i}-shell-completion{} and {+i}-shell-candidates{} " "has been renamed\n" "» exclusive face attributes is replaced with final " "(fg/bg/attr)\n" "» {+b}{} (merge consecutive) moved to {+b}{} to make {+b}{} " "backward {+b}{}\n" "» {+u}remove-hooks{} now takes a regex parameter\n" }, { 20180904, "» Big breaking refactoring of various Kakoune features, " "configuration might need to be updated see `:doc changelog` " "for details\n" "» {+u}define-command{} {+i}-allow-override{} switch has been renamed " "{+i}-override{}\n" }, { 20180413, "» {+u}ModeChange{} hook has been introduced and is expected " "to replace the various {+ui}Mode{+u}Begin{}/{+ui}Mode{+u}End{} hooks, " "consider those deprecated.\n" "» {+b}*{} Does not strip whitespaces anymore, use built-in " "{+b}_{} to strip them\n" "» {+b}l{} on eol will go to next line, {+b}h{} on first char will " "go to previous\n" "» selections merging behaviour is now a bit more complex " "again\n" "» {+b}x{} will only jump to next line if full line is already " "selected\n" "» {+i}WORD{} text object moved to {+b}{} instead of {+b}W{} for " "consistency\n" "» rotate main selection moved to {+b}){}, rotate content to {+b}{}, " "{+b}({} for backward\n" "» faces are now scoped, {+u}set-face{} command takes an additional " "scope parameter\n" "» {+b}{} key is gone, use {+b}{} instead\n" } }; void show_startup_info(Client* local_client, int last_version) { const Face version_face{Color::Default, Color::Default, Attribute::Bold}; DisplayLineList info; for (auto note : version_notes) { if (note.version and note.version <= last_version) continue; if (not note.version) info.push_back({"• Development version", version_face}); else { const auto year = note.version / 10000; const auto month = (note.version / 100) % 100; const auto day = note.version % 100; info.push_back({format("• Kakoune v{}.{}{}.{}{}", year, month < 10 ? "0" : "", month, day < 10 ? "0" : "", day), version_face}); } for (auto&& line : note.notes | split('\n')) info.push_back(parse_display_line(line, GlobalScope::instance().faces())); } if (not info.empty()) { info.push_back({"See the `:doc options startup-info` to control this message", Face{Color::Default, Color::Default, Attribute::Italic}}); local_client->info_show({format("Kakoune {}", version), version_face}, info, {}, InfoStyle::Prompt); } } inline void write_stdout(StringView str) { try { write(STDOUT_FILENO, str); } catch (runtime_error&) {} } inline void write_stderr(StringView str) { try { write(STDERR_FILENO, str); } catch (runtime_error&) {} } String runtime_directory() { if (const char* runtime_directory = getenv("KAKOUNE_RUNTIME")) return runtime_directory; char relpath[PATH_MAX+1]; format_to(relpath, "{}../share/kak", split_path(get_kak_binary_path()).first); struct stat st; if (stat(relpath, &st) == 0 and S_ISDIR(st.st_mode)) return real_path(relpath); return "/usr/share/kak"; } String config_directory() { if (StringView kak_cfg_dir = getenv("KAKOUNE_CONFIG_DIR"); not kak_cfg_dir.empty()) return kak_cfg_dir.str(); if (StringView xdg_cfg_home = getenv("XDG_CONFIG_HOME"); not xdg_cfg_home.empty()) return format("{}/kak", xdg_cfg_home); return format("{}/.config/kak", homedir()); } static auto main_sel_first(const SelectionList& selections) { auto beg = &*selections.begin(), end = &*selections.end(); auto main = beg + selections.main_index(); using View = ConstArrayView; return concatenated(View{main, end}, View{beg, main}); } static const EnvVarDesc builtin_env_vars[] = { { "bufname", false, [](StringView name, const Context& context) -> Vector { return {context.buffer().display_name()}; } }, { "buffile", false, [](StringView name, const Context& context) -> Vector { return {context.buffer().name()}; } }, { "buflist", false, [](StringView name, const Context& context) -> Vector { return BufferManager::instance() | transform(&Buffer::display_name) | gather(); } }, { "buf_line_count", false, [](StringView name, const Context& context) -> Vector { return {to_string(context.buffer().line_count())}; } }, { "timestamp", false, [](StringView name, const Context& context) -> Vector { return {to_string(context.buffer().timestamp())}; } }, { "history_id", false, [](StringView name, const Context& context) -> Vector { return {to_string((size_t)context.buffer().current_history_id())}; } }, { "selection", false, [](StringView name, const Context& context) -> Vector { const Selection& sel = context.selections().main(); return {content(context.buffer(), sel)}; } }, { "selections", false, [](StringView name, const Context& context) -> Vector { return context.selections_content(); } }, { "runtime", false, [](StringView name, const Context& context) -> Vector { return {runtime_directory()}; } }, { "config", false, [](StringView name, const Context& context) -> Vector { return {config_directory()}; } }, { "version", false, [](StringView name, const Context& context) -> Vector { return {version}; } }, { "opt_", true, [](StringView name, const Context& context) -> Vector { return context.options()[name.substr(4_byte)].get_as_strings(); } }, { "main_reg_", true, [](StringView name, const Context& context) -> Vector { return {context.main_sel_register_value(name.substr(9_byte)).str()}; } }, { "reg_", true, [](StringView name, const Context& context) { return RegisterManager::instance()[name.substr(4_byte)].get(context) | gather>(); } }, { "client_env_", true, [](StringView name, const Context& context) -> Vector { return {context.client().get_env_var(name.substr(11_byte)).str()}; } }, { "session", false, [](StringView name, const Context& context) -> Vector { return {Server::instance().session()}; } }, { "client", false, [](StringView name, const Context& context) -> Vector { return {context.name()}; } }, { "client_pid", false, [](StringView name, const Context& context) -> Vector { return {to_string(context.client().pid())}; } }, { "client_list", false, [](StringView name, const Context& context) -> Vector { return ClientManager::instance() | transform([](const std::unique_ptr& c) -> const String& { return c->context().name(); }) | gather(); } }, { "modified", false, [](StringView name, const Context& context) -> Vector { return {context.buffer().is_modified() ? "true" : "false"}; } }, { "cursor_line", false, [](StringView name, const Context& context) -> Vector { return {to_string(context.selections().main().cursor().line + 1)}; } }, { "cursor_column", false, [](StringView name, const Context& context) -> Vector { return {to_string(context.selections().main().cursor().column + 1)}; } }, { "cursor_char_value", false, [](StringView name, const Context& context) -> Vector { auto coord = context.selections().main().cursor(); auto& buffer = context.buffer(); return {to_string((size_t)utf8::codepoint(buffer.iterator_at(coord), buffer.end()))}; } }, { "cursor_char_column", false, [](StringView name, const Context& context) -> Vector { auto coord = context.selections().main().cursor(); return {to_string(context.buffer()[coord.line].char_count_to(coord.column) + 1)}; } }, { "cursor_display_column", false, [](StringView name, const Context& context) -> Vector { auto coord = context.selections().main().cursor(); return {to_string(get_column(context.buffer(), context.options()["tabstop"].get(), coord) + 1)}; } }, { "cursor_byte_offset", false, [](StringView name, const Context& context) -> Vector { auto cursor = context.selections().main().cursor(); return {to_string(context.buffer().distance({0,0}, cursor))}; } }, { "selection_desc", false, [](StringView name, const Context& context) -> Vector { return {selection_to_string(ColumnType::Byte, context.buffer(), context.selections().main())}; } }, { "selections_desc", false, [](StringView name, const Context& context) -> Vector { return main_sel_first(context.selections()) | transform([&buffer=context.buffer()](const Selection& sel) { return selection_to_string(ColumnType::Byte, buffer, sel); }) | gather(); } }, { "selections_char_desc", false, [](StringView name, const Context& context) -> Vector { return main_sel_first(context.selections()) | transform([&buffer=context.buffer()](const Selection& sel) { return selection_to_string(ColumnType::Codepoint, buffer, sel); }) | gather(); } }, { "selections_display_column_desc", false, [](StringView name, const Context& context) -> Vector { return main_sel_first(context.selections()) | transform([&buffer=context.buffer(), tabstop=context.options()["tabstop"].get()](const Selection& sel) { return selection_to_string(ColumnType::DisplayColumn, buffer, sel, tabstop); }) | gather(); } }, { "selection_length", false, [](StringView name, const Context& context) -> Vector { return {to_string(char_length(context.buffer(), context.selections().main()))}; } }, { "selections_length", false, [](StringView name, const Context& context) -> Vector { return context.selections() | transform([&](const Selection& s) -> String { return to_string(char_length(context.buffer(), s)); }) | gather>(); } }, { "window_width", false, [](StringView name, const Context& context) -> Vector { return {to_string(context.window().dimensions().column)}; } }, { "window_height", false, [](StringView name, const Context& context) -> Vector { return {to_string(context.window().dimensions().line)}; } }, { "user_modes", false, [](StringView name, const Context& context) -> Vector { return context.keymaps().user_modes(); } }, { "window_range", false, [](StringView name, const Context& context) -> Vector { auto setup = context.window().compute_display_setup(context); return {format("{} {} {} {}", setup.window_pos.line, setup.window_pos.column, setup.window_range.line, setup.window_range.column)}; } }, { "history", false, [](StringView name, const Context& context) -> Vector { return history_as_strings(context.buffer().history()); } }, { "uncommitted_modifications", false, [](StringView name, const Context& context) -> Vector { return undo_group_as_strings(context.buffer().current_undo_group()); } } }; void register_registers() { RegisterManager& register_manager = RegisterManager::instance(); for (Codepoint c : StringView{"abcdefghijklmnopqrstuvwxyz\"^@"}) register_manager.add_register(c, std::make_unique(String{c})); for (Codepoint c : StringView{"/|:\\"}) register_manager.add_register(c, std::make_unique(String{c})); using StringList = Vector; register_manager.add_register('%', make_dyn_reg( "%", [](const Context& context) { return StringList{{context.buffer().display_name()}}; })); register_manager.add_register('.', make_dyn_reg( ".", [](const Context& context) { auto content = context.selections_content(); return StringList{content.begin(), content.end()}; })); register_manager.add_register('#', make_dyn_reg( "#", [](const Context& context) { const size_t count = context.selections().size(); StringList res; res.reserve(count); for (size_t i = 1; i < count+1; ++i) res.push_back(to_string((int)i)); return res; })); for (size_t i = 0; i < 10; ++i) { register_manager.add_register('0'+i, make_dyn_reg( String{Codepoint('0'+i)}, [i](const Context& context) { StringList result; for (auto& sel : context.selections()) result.emplace_back(i < sel.captures().size() ? sel.captures()[i] : ""); return result; }, [i](Context& context, ConstArrayView values) { if (values.empty()) return; auto& sels = context.selections(); for (size_t sel_index = 0; sel_index < sels.size(); ++sel_index) { auto& sel = sels[sel_index]; if (sel.captures().size() < i+1) sel.captures().resize(i+1); sel.captures()[i] = values[std::min(sel_index, values.size()-1)]; } })); } register_manager.add_register('_', std::make_unique()); } void register_keymaps() { auto& keymaps = GlobalScope::instance().keymaps(); keymaps.map_key(Key::Left, KeymapMode::Normal, {'h'}, ""); keymaps.map_key(Key::Right, KeymapMode::Normal, {'l'}, ""); keymaps.map_key(Key::Down, KeymapMode::Normal, {'j'}, ""); keymaps.map_key(Key::Up, KeymapMode::Normal, {'k'}, ""); keymaps.map_key(shift(Key::Left), KeymapMode::Normal, {'H'}, ""); keymaps.map_key(shift(Key::Right), KeymapMode::Normal, {'L'}, ""); keymaps.map_key(shift(Key::Down), KeymapMode::Normal, {'J'}, ""); keymaps.map_key(shift(Key::Up), KeymapMode::Normal, {'K'}, ""); keymaps.map_key(Key::End, KeymapMode::Normal, {alt('l')}, ""); keymaps.map_key(Key::Home, KeymapMode::Normal, {alt('h')}, ""); keymaps.map_key(shift(Key::End), KeymapMode::Normal, {alt('L')}, ""); keymaps.map_key(shift(Key::Home), KeymapMode::Normal, {alt('H')}, ""); } static void check_tabstop(const int& val) { if (val < 1) throw runtime_error{"tabstop should be strictly positive"}; } static void check_indentwidth(const int& val) { if (val < 0) throw runtime_error{"indentwidth should be positive or zero"}; } static void check_scrolloff(const DisplayCoord& so) { if (so.line < 0 or so.column < 0) throw runtime_error{"scroll offset must be positive or zero"}; } static void check_timeout(const int& timeout) { if (timeout < 50) throw runtime_error{"the minimum acceptable timeout is 50 milliseconds"}; } static void check_extra_word_chars(const Vector& extra_chars) { if (any_of(extra_chars, is_blank)) throw runtime_error{"blanks are not accepted for extra completion characters"}; } static void check_matching_pairs(const Vector& pairs) { if ((pairs.size() % 2) != 0) throw runtime_error{"matching pairs should have a pair number of element"}; if (not all_of(pairs, [](Codepoint cp) { return is_punctuation(cp); })) throw runtime_error{"matching pairs can only be punctuation"}; } void register_options() { OptionsRegistry& reg = GlobalScope::instance().option_registry(); reg.declare_option("tabstop", "size of a tab character", 8); reg.declare_option("indentwidth", "indentation width", 4); reg.declare_option( "scrolloff", "number of lines and columns to keep visible main cursor when scrolling", {0,0}); reg.declare_option("eolformat", "end of line format", EolFormat::Lf); reg.declare_option("BOM", "byte order mark to use when writing buffer", ByteOrderMark::None); reg.declare_option("incsearch", "incrementally apply search/select/split regex", true); reg.declare_option("autoinfo", "automatically display contextual help", AutoInfo::Command | AutoInfo::OnKey); reg.declare_option("autocomplete", "automatically display possible completions", AutoComplete::Insert | AutoComplete::Prompt); reg.declare_option("aligntab", "use tab characters when possible for alignment", false); reg.declare_option("ignored_files", "patterns to ignore when completing filenames", Regex{R"(^(\..*|.*\.(o|so|a))$)"}); reg.declare_option("disabled_hooks", "patterns to disable hooks whose group is matched", Regex{}); reg.declare_option("filetype", "buffer filetype", ""_str); reg.declare_option("path", "path to consider when trying to find a file", Vector({ "./", "%/", "/usr/include" })); reg.declare_option("completers", "insert mode completers to execute.", InsertCompleterDescList({ InsertCompleterDesc{ InsertCompleterDesc::Filename, {} }, InsertCompleterDesc{ InsertCompleterDesc::Word, "all"_str } }), OptionFlags::None); reg.declare_option("static_words", "list of words to always consider for insert word completion", Vector{}); reg.declare_option("autoreload", "autoreload buffer when a filesystem modification is detected", Autoreload::Ask); reg.declare_option("writemethod", "how to write buffer to files", WriteMethod::Overwrite); reg.declare_option( "idle_timeout", "timeout, in milliseconds, before idle hooks are triggered", 50); reg.declare_option( "fs_check_timeout", "timeout, in milliseconds, between file system buffer modification checks", 500); reg.declare_option("ui_options", "space separated list of = options that are " "passed to and interpreted by the user interface\n" "\n" "The ncurses ui supports the following options:\n" " : :\n" " ncurses_assistant clippy|cat|dilbert|none|off\n" " ncurses_status_on_top bool\n" " ncurses_set_title bool\n" " ncurses_enable_mouse bool\n" " ncurses_change_colors bool\n" " ncurses_wheel_up_button int\n" " ncurses_wheel_down_button int\n" " ncurses_wheel_scroll_amount int\n" " ncurses_shift_function_key int\n", UserInterface::Options{}); reg.declare_option("modelinefmt", "format string used to generate the modeline", "%val{bufname} %val{cursor_line}:%val{cursor_char_column} {{context_info}} {{mode_info}} - %val{client}@[%val{session}]"_str); reg.declare_option("debug", "various debug flags", DebugFlags::None); reg.declare_option("readonly", "prevent buffers from being modified", false); reg.declare_option, check_extra_word_chars>( "extra_word_chars", "Additional characters to be considered as words for insert completion", { '_' }); reg.declare_option, check_matching_pairs>( "matching_pairs", "set of pair of characters to be considered as matching pairs", { '(', ')', '{', '}', '[', ']', '<', '>' }); reg.declare_option("startup_info_version", "version up to which startup info changes should be hidden", 0); } static Client* local_client = nullptr; static int local_client_exit = 0; static bool convert_to_client_pending = false; enum class UIType { NCurses, Json, Dummy, }; UIType parse_ui_type(StringView ui_name) { if (ui_name == "ncurses") return UIType::NCurses; if (ui_name == "json") return UIType::Json; if (ui_name == "dummy") return UIType::Dummy; throw parameter_error(format("error: unknown ui type: '{}'", ui_name)); } std::unique_ptr make_ui(UIType ui_type) { struct DummyUI : UserInterface { DummyUI() { set_signal_handler(SIGINT, SIG_DFL); } bool is_ok() const override { return true; } void menu_show(ConstArrayView, DisplayCoord, Face, Face, MenuStyle) override {} void menu_select(int) override {} void menu_hide() override {} void info_show(const DisplayLine&, const DisplayLineList&, DisplayCoord, Face, InfoStyle) override {} void info_hide() override {} void draw(const DisplayBuffer&, const Face&, const Face&) override {} void draw_status(const DisplayLine&, const DisplayLine&, const Face&) override {} DisplayCoord dimensions() override { return {24,80}; } void set_cursor(CursorMode, DisplayCoord) override {} void refresh(bool) override {} void set_on_key(OnKeyCallback) override {} void set_ui_options(const Options&) override {} }; switch (ui_type) { case UIType::NCurses: return std::make_unique(); case UIType::Json: return std::make_unique(); case UIType::Dummy: return std::make_unique(); } throw logic_error{}; } pid_t fork_server_to_background() { if (pid_t pid = fork()) return pid; setsid(); if (fork()) // double fork to orphan the server exit(0); write_stderr(format("Kakoune forked server to background ({}), for session '{}'\n", getpid(), Server::instance().session())); return 0; } std::unique_ptr create_local_ui(UIType ui_type) { if (ui_type != UIType::NCurses) return make_ui(ui_type); struct LocalUI : NCursesUI { LocalUI() { set_signal_handler(SIGTSTP, [](int) { if (ClientManager::instance().count() == 1 and *ClientManager::instance().begin() == local_client) NCursesUI::instance().suspend(); else convert_to_client_pending = true; }); } ~LocalUI() override { local_client = nullptr; if (convert_to_client_pending or ClientManager::instance().empty()) return; if (fork_server_to_background()) { this->NCursesUI::~NCursesUI(); exit(local_client_exit); } } }; if (not isatty(0)) { // move stdin to another fd, and restore tty as stdin int fd = dup(0); int tty = open("/dev/tty", O_RDONLY); dup2(tty, 0); close(tty); create_fifo_buffer("*stdin*", fd, Buffer::Flags::None); } return std::make_unique(); } int run_client(StringView session, StringView name, StringView client_init, Optional init_coord, UIType ui_type, bool suspend) { try { Optional stdin_fd; if (not isatty(0)) { // move stdin to another fd, and restore tty as stdin stdin_fd = dup(0); int tty = open("/dev/tty", O_RDONLY); dup2(tty, 0); close(tty); } EventManager event_manager; RemoteClient client{session, name, make_ui(ui_type), getpid(), get_env_vars(), client_init, std::move(init_coord), stdin_fd}; stdin_fd.map(close); if (suspend) raise(SIGTSTP); while (not client.exit_status() and client.is_ui_ok()) event_manager.handle_next_events(EventMode::Normal); return client.exit_status().value_or(-1); } catch (disconnected& e) { write_stderr(format("{}\ndisconnecting\n", e.what())); return -1; } } struct convert_to_client_mode { String session; String client_name; String buffer_name; String selections; }; enum class ServerFlags { None = 0, IgnoreKakrc = 1 << 0, Daemon = 1 << 1, ReadOnly = 1 << 2, StartupInfo = 1 << 3, }; constexpr bool with_bit_ops(Meta::Type) { return true; } int run_server(StringView session, StringView server_init, StringView client_init, Optional init_coord, ServerFlags flags, UIType ui_type, DebugFlags debug_flags, ConstArrayView files) { static bool terminate = false; set_signal_handler(SIGTERM, [](int) { terminate = true; }); set_signal_handler(SIGINT, [](int) { terminate = true; }); if ((flags & ServerFlags::Daemon) and session.empty()) { write_stderr("-d needs a session name to be specified with -s\n"); return -1; } EventManager event_manager; Server server{session.empty() ? to_string(getpid()) : session.str(), (bool)(flags & ServerFlags::Daemon)}; StringRegistry string_registry; GlobalScope global_scope; ShellManager shell_manager{builtin_env_vars}; CommandManager command_manager; RegisterManager register_manager; HighlighterRegistry highlighter_registry; SharedHighlighters defined_highlighters; ClientManager client_manager; BufferManager buffer_manager; register_options(); register_registers(); register_keymaps(); register_commands(); register_highlighters(); global_scope.options()["debug"].set(debug_flags); write_to_debug_buffer("*** This is the debug buffer, where debug info will be written ***"); const auto start_time = Clock::now(); UnitTest::run_all_tests(); if (debug_flags & DebugFlags::Profile) { using namespace std::chrono; write_to_debug_buffer(format("running the unit tests took {} ms", duration_cast(Clock::now() - start_time).count())); } GlobalScope::instance().options().get_local_option("readonly").set(flags & ServerFlags::ReadOnly); bool startup_error = false; if (not (flags & ServerFlags::IgnoreKakrc)) try { Context init_context{Context::EmptyContextFlag{}}; command_manager.execute(format("source {}/kakrc", runtime_directory()), init_context); } catch (runtime_error& error) { startup_error = true; write_to_debug_buffer(format("error while parsing kakrc:\n" " {}", error.what())); } if (not server_init.empty()) try { Context init_context{Context::EmptyContextFlag{}}; command_manager.execute(server_init, init_context); } catch (runtime_error& error) { startup_error = true; write_to_debug_buffer(format("error while running server init commands:\n" " {}", error.what())); } { Context empty_context{Context::EmptyContextFlag{}}; global_scope.hooks().run_hook(Hook::KakBegin, session, empty_context); } if (not files.empty()) try { // create buffers in reverse order so that the first given buffer // is the most recently created one. for (auto& file : files | reverse()) { try { Buffer *buffer = open_or_create_file_buffer(file); if (flags & ServerFlags::ReadOnly) buffer->flags() |= Buffer::Flags::ReadOnly; } catch (runtime_error& error) { startup_error = true; write_to_debug_buffer(format("error while opening file '{}':\n" " {}", file, error.what())); } } } catch (runtime_error& error) { write_to_debug_buffer(format("error while opening command line files: {}", error.what())); } try { if (not server.is_daemon()) { local_client = client_manager.create_client( create_local_ui(ui_type), getpid(), {}, get_env_vars(), client_init, std::move(init_coord), [](int status) { local_client_exit = status; }); if (startup_error and local_client) local_client->print_status({ "error during startup, see `:buffer *debug*` for details", local_client->context().faces()["Error"] }); if (flags & ServerFlags::StartupInfo and local_client) show_startup_info(local_client, global_scope.options()["startup_info_version"].get()); } while (not terminate and (not client_manager.empty() or server.negotiating() or server.is_daemon())) { client_manager.redraw_clients(); // Loop so that eventual inputs happening during the processing are handled as // well, avoiding unneeded redraws. bool allow_blocking = not client_manager.has_pending_inputs(); while (event_manager.handle_next_events(EventMode::Normal, nullptr, allow_blocking)) { if (client_manager.process_pending_inputs()) break; allow_blocking = false; } client_manager.process_pending_inputs(); client_manager.clear_client_trash(); client_manager.clear_window_trash(); buffer_manager.clear_buffer_trash(); global_scope.option_registry().clear_option_trash(); if (local_client and not contains(client_manager, local_client)) local_client = nullptr; else if (local_client and not local_client->is_ui_ok()) { ClientManager::instance().remove_client(*local_client, false, -1); local_client = nullptr; if (not client_manager.empty() and fork_server_to_background()) return 0; } else if (convert_to_client_pending) { kak_assert(local_client); auto& local_context = local_client->context(); const String client_name = local_context.name(); const String buffer_name = local_context.buffer().name(); const String selections = selection_list_to_string(ColumnType::Byte, local_context.selections()); ClientManager::instance().remove_client(*local_client, true, 0); client_manager.clear_client_trash(); convert_to_client_pending = false; if (fork_server_to_background()) { ClientManager::instance().clear(false); String session = server.session(); server.close_session(false); throw convert_to_client_mode{ std::move(session), std::move(client_name), std::move(buffer_name), std::move(selections) }; } } } } catch (const kill_session& kill) { local_client_exit = kill.exit_status; } { Context empty_context{Context::EmptyContextFlag{}}; global_scope.hooks().run_hook(Hook::KakEnd, "", empty_context); } return local_client_exit; } int run_filter(StringView keystr, ConstArrayView files, bool quiet, StringView suffix_backup) { StringRegistry string_registry; GlobalScope global_scope; EventManager event_manager; ShellManager shell_manager{builtin_env_vars}; RegisterManager register_manager; BufferManager buffer_manager; register_options(); register_registers(); try { auto keys = parse_keys(keystr); auto apply_to_buffer = [&](Buffer& buffer) { try { InputHandler input_handler{ { buffer, Selection{{0,0}, buffer.back_coord()} }, Context::Flags::Draft }; for (auto& key : keys) input_handler.handle_key(key); } catch (runtime_error& err) { if (not quiet) write_stderr(format("error while applying keys to buffer '{}': {}\n", buffer.display_name(), err.what())); } }; for (auto& file : files) { Buffer* buffer = open_file_buffer(file, Buffer::Flags::NoHooks); if (not suffix_backup.empty()) write_buffer_to_file(*buffer, buffer->name() + suffix_backup, WriteMethod::Overwrite, WriteFlags::None); apply_to_buffer(*buffer); write_buffer_to_file(*buffer, buffer->name(), WriteMethod::Overwrite, WriteFlags::None); buffer_manager.delete_buffer(*buffer); } if (not isatty(0)) { Buffer& buffer = *create_buffer_from_string( "*stdin*", Buffer::Flags::NoHooks, read_fd(0)); apply_to_buffer(buffer); write_buffer_to_fd(buffer, 1); buffer_manager.delete_buffer(buffer); } } catch (runtime_error& err) { write_stderr(format("error: {}\n", err.what())); } buffer_manager.clear_buffer_trash(); return 0; } int run_pipe(StringView session) { try { send_command(session, read_fd(0)); } catch (disconnected& e) { write_stderr(format("{}\ndisconnecting\n", e.what())); return -1; } return 0; } void signal_handler(int signal) { NCursesUI::abort(); const char* text = nullptr; switch (signal) { case SIGSEGV: text = "SIGSEGV"; break; case SIGFPE: text = "SIGFPE"; break; case SIGQUIT: text = "SIGQUIT"; break; case SIGPIPE: text = "SIGPIPE"; break; } auto msg = format("Received {}, exiting.\nPid: {}\nCallstack:\n{}", text, getpid(), Backtrace{}.desc()); write_stderr(msg); notify_fatal_error(msg); if (Server::has_instance()) Server::instance().close_session(); if (BufferManager::has_instance()) BufferManager::instance().backup_modified_buffers(); if (signal == SIGSEGV) { // generate core dump ::signal(SIGSEGV, SIG_DFL); ::kill(getpid(), SIGSEGV); } else abort(); } } int main(int argc, char* argv[]) { using namespace Kakoune; setlocale(LC_ALL, ""); set_signal_handler(SIGSEGV, signal_handler); set_signal_handler(SIGFPE, signal_handler); set_signal_handler(SIGQUIT, signal_handler); set_signal_handler(SIGTERM, signal_handler); set_signal_handler(SIGPIPE, [](int){}); set_signal_handler(SIGINT, [](int){}); set_signal_handler(SIGCHLD, [](int){}); set_signal_handler(SIGTTOU, SIG_IGN); const ParameterDesc param_desc{ SwitchMap{ { "c", { true, "connect to given session" } }, { "e", { true, "execute argument on client initialisation" } }, { "E", { true, "execute argument on server initialisation" } }, { "n", { false, "do not source kakrc files on startup" } }, { "s", { true, "set session name" } }, { "d", { false, "run as a headless session (requires -s)" } }, { "p", { true, "just send stdin as commands to the given session" } }, { "f", { true, "filter: for each file, select the entire buffer and execute the given keys" } }, { "i", { true, "backup the files on which a filter is applied using the given suffix" } }, { "q", { false, "in filter mode, be quiet about errors applying keys" } }, { "ui", { true, "set the type of user interface to use (ncurses, dummy, or json)" } }, { "l", { false, "list existing sessions" } }, { "clear", { false, "clear dead sessions" } }, { "debug", { true, "initial debug option value" } }, { "version", { false, "display kakoune version and exit" } }, { "ro", { false, "readonly mode" } }, { "help", { false, "display a help message and quit" } } } }; try { auto show_usage = [&]() { write_stdout(format("Usage: {} [options] [file]... [+[:]|+:]\n\n" "Options:\n" "{}\n" "Prefixing a positional argument with a plus (`+`) sign will place the\n" "cursor at a given set of coordinates, or the end of the buffer if the plus\n" "sign is followed only by a colon (`:`)\n", argv[0], generate_switches_doc(param_desc.switches))); return 0; }; const auto params = ArrayView{argv+1, argv + argc} | transform([](auto* s) { return String{s}; }) | gather>(); if (contains(params, "--help"_sv)) return show_usage(); ParametersParser parser{params, param_desc}; const bool show_help_message = (bool)parser.get_switch("help"); if (show_help_message) return show_usage(); if (parser.get_switch("version")) { write_stdout(format("Kakoune {}\n", Kakoune::version)); return 0; } const bool list_sessions = (bool)parser.get_switch("l"); const bool clear_sessions = (bool)parser.get_switch("clear"); if (list_sessions or clear_sessions) { for (auto& session : list_files(session_directory())) { const bool valid = check_session(session); if (list_sessions) write_stdout(format("{}{}\n", session, valid ? "" : " (dead)")); if (not valid and clear_sessions) unlink(session_path(session).c_str()); } return 0; } if (auto session = parser.get_switch("p")) { for (auto opt : { "c", "n", "s", "d", "e", "E", "ro" }) { if (parser.get_switch(opt)) { write_stderr(format("error: -{} is incompatible with -p\n", opt)); return -1; } } return run_pipe(*session); } auto client_init = parser.get_switch("e").value_or(StringView{}); auto server_init = parser.get_switch("E").value_or(StringView{}); const UIType ui_type = parse_ui_type(parser.get_switch("ui").value_or("ncurses")); if (auto keys = parser.get_switch("f")) { if (parser.get_switch("ro")) { write_stderr("error: -ro is incompatible with -f\n"); return -1; } Vector files; for (size_t i = 0; i < parser.positional_count(); ++i) files.emplace_back(parser[i]); return run_filter(*keys, files, (bool)parser.get_switch("q"), parser.get_switch("i").value_or(StringView{})); } Vector files; Optional init_coord; for (auto& name : parser) { if (not name.empty() and name[0_byte] == '+') { if (name == "+" or name == "+:") { client_init = client_init + "; exec gj"; continue; } auto colon = find(name, ':'); if (auto line = str_to_int_ifp({name.begin()+1, colon})) { init_coord = std::max({0,0}, { *line - 1, colon != name.end() ? str_to_int_ifp({colon+1, name.end()}).value_or(1) - 1 : 0 }); continue; } } files.emplace_back(name); } if (auto server_session = parser.get_switch("c")) { for (auto opt : { "n", "s", "d", "E", "ro" }) { if (parser.get_switch(opt)) { write_stderr(format("error: -{} is incompatible with -c\n", opt)); return -1; } } String new_files; for (auto name : files) { new_files += format("edit '{}'", escape(real_path(name), "'", '\\')); if (init_coord) { new_files += format(" {} {}", init_coord->line + 1, init_coord->column + 1); init_coord.reset(); } new_files += ";"; } return run_client(*server_session, {}, new_files + client_init, init_coord, ui_type, false); } else { StringView session = parser.get_switch("s").value_or(StringView{}); try { auto ignore_kakrc = (bool)parser.get_switch("n"); auto flags = (ignore_kakrc ? ServerFlags::IgnoreKakrc : ServerFlags::None) | (parser.get_switch("d") ? ServerFlags::Daemon : ServerFlags::None) | (parser.get_switch("ro") ? ServerFlags::ReadOnly : ServerFlags::None) | ((argc == 1 or (ignore_kakrc and argc == 2)) and isatty(0) ? ServerFlags::StartupInfo : ServerFlags::None); auto debug_flags = option_from_string(Meta::Type{}, parser.get_switch("debug").value_or("")); return run_server(session, server_init, client_init, init_coord, flags, ui_type, debug_flags, files); } catch (convert_to_client_mode& convert) { return run_client(convert.session, convert.client_name, format("try %^buffer '{}'; select '{}'^; echo converted to client only mode", escape(convert.buffer_name, "'^", '\\'), convert.selections), {}, ui_type, true); } } } catch (parameter_error& error) { write_stderr(format("Error while parsing parameters: {}\n" "Valid switches:\n" "{}", error.what(), generate_switches_doc(param_desc.switches))); return -1; } catch (Kakoune::exception& error) { write_stderr(format("Fatal error: {}\n", error.what())); return -1; } catch (std::exception& error) { write_stderr(format("uncaught exception ({}):\n{}\n", typeid(error).name(), error.what())); return -1; } catch (...) { write_stderr("uncaught exception"); return -1; } return 0; } #if defined(__ELF__) asm(R"( .pushsection ".debug_gdb_scripts", "MS",@progbits,1 .byte 4 .ascii "kakoune-inline-gdb.py\n" .ascii "import os.path\n" .ascii "sys.path.insert(0, os.path.dirname(gdb.current_objfile().filename) + '/../share/kak/gdb/')\n" .ascii "import gdb.printing\n" .ascii "import kakoune\n" .ascii "gdb.printing.register_pretty_printer(gdb.current_objfile(), kakoune.build_pretty_printer())\n\0" .popsection )"); #endif