diff --git a/README.asciidoc b/README.asciidoc index f36e69aa64..2e2e2e69ff 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -968,7 +968,10 @@ Some options are built in Kakoune, and can be used to control it's behaviour: on the filesystem. * `modelinefmt` _string_: A format string used to generate the mode line, that string is first expanded as a command line would be (expanding `%...{...}` - strings), then markup tags are applied (see <>). + strings), then markup tags are applied (see <>). Two special + atom are available as markup: `{{mode_info}}` with information about the current + mode (example `insert 3 sel`), and `{{context_info}}` with information such as + if the file has been modified (with `[+]`), or if it is new (with `[new file]`). * `ui_options`: colon separated list of key=value pairs that are forwarded to the user interface implementation. The NCurses UI support the following options: - `ncurses_set_title`: if `yes` or `true`, the terminal emulator title will diff --git a/doc/manpages/options.asciidoc b/doc/manpages/options.asciidoc index 20fa5ce85a..d27c54c93f 100644 --- a/doc/manpages/options.asciidoc +++ b/doc/manpages/options.asciidoc @@ -137,7 +137,19 @@ Builtin options *modelinefmt* 'string':: A format string used to generate the mode line, that string is first expanded as a command line would be (expanding '%...{...}' strings), - then markup tags are applied (c.f. the 'Expansions' documentation page) + then markup tags are applied (c.f. the 'Expansions' documentation page.) + Two special atoms are available as markup: + + *`{{mode_info}}`*::: + Information about the current mode, such as `insert 3 sel` or + `prompt`. The faces used are StatusLineMode, StatusLineInfo, + and StatusLineValue. + + *`{{context_info}}`*::: + Information such as `[+][recording (@)][no-hooks][new file][fifo]`, + in face Information. + + The default value is '%val{bufname} %val{cursor_line}:%val{cursor_char_column} {{context_info}} {{mode_info}} - %val{client}@[%val{session}]' *ui_options*:: colon separated list of key=value pairs that are forwarded to the user diff --git a/src/client.cc b/src/client.cc index 638c230cd2..e9da59ff5d 100644 --- a/src/client.cc +++ b/src/client.cc @@ -11,6 +11,7 @@ #include "event_manager.hh" #include "user_interface.hh" #include "window.hh" +#include "hash_map.hh" #include #include @@ -117,15 +118,33 @@ DisplayCoord Client::dimensions() const return m_ui->dimensions(); } +String generate_context_info(const Context& context) +{ + String s = ""; + if (context.buffer().is_modified()) + s += "[+]"; + if (context.client().input_handler().is_recording()) + s += format("[recording ({})]", context.client().input_handler().recording_reg()); + if (context.buffer().flags() & Buffer::Flags::New) + s += "[new file]"; + if (context.hooks_disabled()) + s += "[no-hooks]"; + if (context.buffer().flags() & Buffer::Flags::Fifo) + s += "[fifo]"; + return s; +} + DisplayLine Client::generate_mode_line() const { DisplayLine modeline; try { const String& modelinefmt = context().options()["modelinefmt"].get(); - - modeline = parse_display_line(expand(modelinefmt, context(), ShellContext{}, - [](String s) { return escape(s, '{', '\\'); })); + HashMap atoms{{ "mode_info", context().client().input_handler().mode_line() }, + { "context_info", {generate_context_info(context()), get_face("Information")}}}; + auto expanded = expand(modelinefmt, context(), ShellContext{}, + [](String s) { return escape(s, '{', '\\'); }); + modeline = parse_display_line(expanded, atoms); } catch (runtime_error& err) { @@ -133,23 +152,6 @@ DisplayLine Client::generate_mode_line() const modeline.push_back({ "modelinefmt error, see *debug* buffer", get_face("Error") }); } - Face info_face = get_face("Information"); - - if (context().buffer().is_modified()) - modeline.push_back({ "[+]", info_face }); - if (m_input_handler.is_recording()) - modeline.push_back({ format("[recording ({})]", m_input_handler.recording_reg()), info_face }); - if (context().buffer().flags() & Buffer::Flags::New) - modeline.push_back({ "[new file]", info_face }); - if (context().hooks_disabled()) - modeline.push_back({ "[no-hooks]", info_face }); - if (context().buffer().flags() & Buffer::Flags::Fifo) - modeline.push_back({ "[fifo]", info_face }); - modeline.push_back({ " " }); - for (auto& atom : m_input_handler.mode_line()) - modeline.push_back(std::move(atom)); - modeline.push_back({ format(" - {}@[{}]", context().name(), Server::instance().session()) }); - return modeline; } diff --git a/src/display_buffer.cc b/src/display_buffer.cc index d24d65f233..aa71a31066 100644 --- a/src/display_buffer.cc +++ b/src/display_buffer.cc @@ -286,7 +286,7 @@ void DisplayBuffer::optimize() line.optimize(); } -DisplayLine parse_display_line(StringView line) +DisplayLine parse_display_line(StringView line, HashMap atoms) { DisplayLine res; bool was_antislash = false; @@ -312,7 +312,18 @@ DisplayLine parse_display_line(StringView line) auto closing = std::find(it+1, end, '}'); if (closing == end) throw runtime_error("unclosed face definition"); - face = get_face({it+1, closing}); + if (*(it+1) == '{' and closing+1 != end and *(closing+1) == '}') + { + auto atom_it = atoms.find(String{it+2, closing}); + if (atom_it == atoms.end()) + throw runtime_error(format("undefined atom {}", String{it+2, closing})); + for (auto display_it = atom_it->value.begin(), end = atom_it->value.end(); display_it != end; ++display_it) + res.push_back(*display_it); + // closing is now at the first char of "}}", advance it to the second + ++closing; + } + else + face = get_face({it+1, closing}); it = closing; pos = closing + 1; } diff --git a/src/display_buffer.hh b/src/display_buffer.hh index 39563a1b38..6f1f8acb7c 100644 --- a/src/display_buffer.hh +++ b/src/display_buffer.hh @@ -6,6 +6,7 @@ #include "coord.hh" #include "string.hh" #include "vector.hh" +#include "hash_map.hh" namespace Kakoune { @@ -143,7 +144,7 @@ private: AtomList m_atoms; }; -DisplayLine parse_display_line(StringView line); +DisplayLine parse_display_line(StringView line, HashMap m_atoms = HashMap{}); class DisplayBuffer : public UseMemoryDomain { diff --git a/src/main.cc b/src/main.cc index 0f8f3e4ba0..ed0e7a1abf 100644 --- a/src/main.cc +++ b/src/main.cc @@ -43,7 +43,9 @@ static const char* startup_info = " * The `identifier` face has been replaced with `variable`,\n" " `function` and `module`, update your custom colorschemes\n" " * BufNew and BufOpen hooks have been renamed to BufNewFile\n" -" and BufOpenFile.\n"; +" and BufOpenFile.\n" +" * The status line can be further customized.\n" +" See `help options modelinefmt`.\n"; struct startup_error : runtime_error { @@ -154,7 +156,8 @@ void register_env_vars() "window_height", false, [](StringView name, const Context& context) -> String { return to_string(context.window().dimensions().line); } - } }; + } + }; ShellManager& shell_manager = ShellManager::instance(); for (auto& env_var : env_vars) @@ -306,7 +309,8 @@ void register_options() " ncurses_wheel_down_button int\n", UserInterface::Options{}); reg.declare_option("modelinefmt", "format string used to generate the modeline", - "%val{bufname} %val{cursor_line}:%val{cursor_char_column} "_str); + "%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( diff --git a/test/highlight/regions/display b/test/highlight/regions/display index 48ffd082fe..4a84dcbb43 100644 --- a/test/highlight/regions/display +++ b/test/highlight/regions/display @@ -1,5 +1,5 @@ { "jsonrpc": "2.0", "method": "draw", "params": [[[{ "face": { "fg": "black", "bg": "white", "attributes": [] }, "contents": "\"" }, { "face": { "fg": "green", "bg": "default", "attributes": [] }, "contents": "abcdefgh\"" }, { "face": { "fg": "yellow", "bg": "default", "attributes": [] }, "contents": " hehe " }, { "face": { "fg": "red", "bg": "default", "attributes": [] }, "contents": "${ youhou{hihi} }" }, { "face": { "fg": "yellow", "bg": "default", "attributes": [] }, "contents": "\u000a" }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": "" }]], { "fg": "default", "bg": "default", "attributes": [] }, { "fg": "blue", "bg": "default", "attributes": [] }] } { "jsonrpc": "2.0", "method": "menu_hide", "params": [] } { "jsonrpc": "2.0", "method": "info_hide", "params": [] } -{ "jsonrpc": "2.0", "method": "draw_status", "params": [[], [{ "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": "out 1:1 " }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": " " }, { "face": { "fg": "blue", "bg": "default", "attributes": [] }, "contents": "1 sel" }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": " - unnamed0@[kak-tests]" }], { "fg": "cyan", "bg": "default", "attributes": [] }] } +{ "jsonrpc": "2.0", "method": "draw_status", "params": [[], [{ "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": "out 1:1 " }, { "face": { "fg": "black", "bg": "yellow", "attributes": [] }, "contents": "" }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": " " }, { "face": { "fg": "blue", "bg": "default", "attributes": [] }, "contents": "1 sel" }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": " - unnamed0@[kak-tests]" }], { "fg": "cyan", "bg": "default", "attributes": [] }] } { "jsonrpc": "2.0", "method": "refresh", "params": [true] } diff --git a/test/regression/638-highlight-codepoint-with-bracket/display b/test/regression/638-highlight-codepoint-with-bracket/display index a98ac64dc4..9406b8e09f 100644 --- a/test/regression/638-highlight-codepoint-with-bracket/display +++ b/test/regression/638-highlight-codepoint-with-bracket/display @@ -1,5 +1,5 @@ { "jsonrpc": "2.0", "method": "draw", "params": [[[{ "face": { "fg": "red", "bg": "default", "attributes": [] }, "contents": "“" }, { "face": { "fg": "white", "bg": "blue", "attributes": [] }, "contents": "We" }, { "face": { "fg": "black", "bg": "white", "attributes": [] }, "contents": " " }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": "ought to scrape this planet clean of every living thing on it," }, { "face": { "fg": "red", "bg": "default", "attributes": [] }, "contents": "”" }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": "\u000a" }]], { "fg": "default", "bg": "default", "attributes": [] }, { "fg": "blue", "bg": "default", "attributes": [] }] } { "jsonrpc": "2.0", "method": "menu_hide", "params": [] } { "jsonrpc": "2.0", "method": "info_hide", "params": [] } -{ "jsonrpc": "2.0", "method": "draw_status", "params": [[], [{ "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": "out 1:4 " }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": " " }, { "face": { "fg": "blue", "bg": "default", "attributes": [] }, "contents": "1 sel" }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": " - unnamed0@[kak-tests]" }], { "fg": "cyan", "bg": "default", "attributes": [] }] } +{ "jsonrpc": "2.0", "method": "draw_status", "params": [[], [{ "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": "out 1:4 " }, { "face": { "fg": "black", "bg": "yellow", "attributes": [] }, "contents": "" }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": " " }, { "face": { "fg": "blue", "bg": "default", "attributes": [] }, "contents": "1 sel" }, { "face": { "fg": "default", "bg": "default", "attributes": [] }, "contents": " - unnamed0@[kak-tests]" }], { "fg": "cyan", "bg": "default", "attributes": [] }] } { "jsonrpc": "2.0", "method": "refresh", "params": [true] }