Skip to content

Commit

Permalink
Merge pull request #1459 from contour-terminal/feature/customizable-s…
Browse files Browse the repository at this point in the history
…tatusline

Add customizable statusline
  • Loading branch information
christianparpart committed Feb 21, 2024
2 parents c9cad17 + 20d94f1 commit fca59ef
Show file tree
Hide file tree
Showing 25 changed files with 1,294 additions and 242 deletions.
74 changes: 74 additions & 0 deletions docs/configuration/indicator-statusline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Indicator Statusline

The indicator statusline used to be a feature, from the old DEC VT level 4 terminals.
Contour revives this feature to prominently show the terminal status.

## Configuration

```
profiles:
your_profile:
status_line:
indicator:
left: "{VTType} │ {InputMode:Bold,Color=#C0C030}{SearchPrompt:Left= │ }{TraceMode:Bold,Color=#FFFF00,Left= │ }{ProtectedMode:Bold,Left= │ }"
middle: "{Title:Left= « ,Right= » ,Color=#20c0c0}"
right: "{HistoryLineCount:Faint,Color=#c0c0c0} │ {Clock:Bold} "
```

Each segment, `left`, `middle`, and `right` may contain text to be displayed in the
left, middle, or right segment of the indicator statusline.

This text may contain placeholders to be replaced by their respective dynamic content.

## Variables

Variable | Description
---------------------|--------------------------------------------------------------------
`{Clock}` | current clock in HH:MM format
`{Command}` | yields the result of the given command prompt, as specified via parameter `Program=...`
`{HistoryLineCount}` | number of lines in history (only available in primary screen)
`{Hyperlink}` | reveals the hyperlink at the given mouse location
`{InputMode}` | current input mode (e.g. INSERT, NORMAL, VISUAL)
`{ProtectedMode}` | indicates protected mode, if currently enabled
`{SearchMode}` | indicates search highlight mode, if currently active
`{SearchPrompt}` | search input prompt, if currently active
`{Text}` | given text (makes only sense when customized with flags)
`{Title}` | current window title
`{VTType}` | currently active VT emulation type

## Formatting Styles

Each Variable, as specified above, can be parametrized for customizing the look of it.
The common syntax to these variables and their parameters looks as follows:

```
{VariableName:SomeFlag,SomeKey=SomeValue}
```

So parameters can be specified after a colon (`:`) as a comma separated list of flags and key/value pairs.
A key/value pair is further split by equal sign (`=`).

The following list of formatting styles are supported:

Parameter | Description
--------------------------|--------------------------------------------------------------------
`Left=TEXT` | text to show on the left side, if the variable is to be shown
`Right=TEXT` | text to show on the right side, if the variable is to be shown
`Color=#RRGGBB` | text color in hexadecimal RGB notation
`BackgroundColor=#RRGGBB` | background color in hexadecimal RGB notation
`Bold` | text in bold font face
`Italic` | text in italic font face
`Underline` | underline text (only one underline style can be active)
`CurlyUnderline` | curly underline text (only one underline style can be active)
`DoubleUnderline` | double underline text (only one underline style can be active)
`DottedUnderline` | dotted underline text (only one underline style can be active)
`DashedUnderline` | dashed underline text (only one underline style can be active)
`Blinking` | blinking text
`RapidBlinking` | rapid blinking text
`Overline` | overline text
`Inverse` | inversed text/background coloring

These parameters apply to all variables above.

The `Command` variable is the only one that requires a special attribute, `Program` whose value
is the command to execute.
1 change: 1 addition & 0 deletions metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
<release version="0.4.4" urgency="medium" type="development">
<description>
<ul>
<li>Add ability to customize the indicator statusline through configuration (#687)</li>
<li>Add generation of config file from internal state (#1282)</li>
<li>Add SGRSAVE and SGRRESTORE VT sequences to save and restore SGR state (They intentionally conflict with XTPUSHSGR and XTPOPSGR)</li>
<li>Fixes corruption of sixel image on high resolution (#1049)</li>
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ nav:
- configuration/index.md
- configuration/profiles.md
- configuration/colors.md
- configuration/indicator-statusline.md
#- Images (Advanced) : configuration/advanced/images.md
#- Mouse (Advanced) : configuration/advanced/images.md
#- Misc (Advanced) : configuration/advanced/mouse.md
Expand Down
15 changes: 15 additions & 0 deletions src/contour/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,13 @@ void YAMLConfigReader::loadFromEntry(YAML::Node const& node, std::string const&
loadFromEntry(child["status_line"], "position", where.statusDisplayPosition);
loadFromEntry(child["status_line"], "sync_to_window_title", where.syncWindowTitleWithHostWritableStatusDisplay);
loadFromEntry(child["status_line"], "display", where.initialStatusDisplayType);

if (child["status_line"]["indicator"])
{
loadFromEntry(child["status_line"]["indicator"], "left", where.indicatorStatusLineLeft);
loadFromEntry(child["status_line"]["indicator"], "middle", where.indicatorStatusLineMiddle);
loadFromEntry(child["status_line"]["indicator"], "right", where.indicatorStatusLineRight);
}
}
if (child["background"])
{
Expand Down Expand Up @@ -1933,6 +1940,14 @@ std::string YAMLConfigWriter::createString(Config const& c)
process(entry.initialStatusDisplayType);
process(entry.statusDisplayPosition);
process(entry.syncWindowTitleWithHostWritableStatusDisplay);

doc.append(addOffset("indicator:\n", Offset::levels * OneOffset));
{
const auto _ = Offset {};
process(entry.indicatorStatusLineLeft);
process(entry.indicatorStatusLineMiddle);
process(entry.indicatorStatusLineRight);
}
}

doc.append(addOffset("\n"
Expand Down
14 changes: 13 additions & 1 deletion src/contour/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,18 @@ struct TerminalProfile
};
ConfigEntry<vtbackend::StatusDisplayPosition, documentation::StatusDisplayPosition>
statusDisplayPosition { vtbackend::StatusDisplayPosition::Bottom };
ConfigEntry<std::string, documentation::IndicatorStatusLineLeft> indicatorStatusLineLeft {
" {InputMode:Bold,Color=#FFFF00}"
"{SearchPrompt:Left= │ }"
"{TraceMode:Bold,Color=#FFFF00,Left= │ }"
"{ProtectedMode:Bold,Left= │ }"
};
ConfigEntry<std::string, documentation::IndicatorStatusLineMiddle> indicatorStatusLineMiddle {
" {Clock:Bold} {Title:Left= « ,Right= » }"
};
ConfigEntry<std::string, documentation::IndicatorStatusLineRight> indicatorStatusLineRight {
"{HistoryLineCount:Faint,Color=#c0c0c0} "
};
ConfigEntry<bool, documentation::SyncWindowTitleWithHostWritableStatusDisplay>
syncWindowTitleWithHostWritableStatusDisplay { false };
ConfigEntry<bool, documentation::HideScrollbarInAltScreen> hideScrollbarInAltScreen { true };
Expand Down Expand Up @@ -351,7 +363,7 @@ struct TerminalProfile
ConfigEntry<std::chrono::milliseconds, documentation::HighlightTimeout> highlightTimeout { 100 };
ConfigEntry<bool, documentation::HighlightDoubleClickerWord> highlightDoubleClickedWord { true };
ConfigEntry<vtbackend::StatusDisplayType, documentation::InitialStatusLine> initialStatusDisplayType {
vtbackend::StatusDisplayType::None
vtbackend::StatusDisplayType::Indicator
};
ConfigEntry<vtbackend::Opacity, documentation::BackgroundOpacity> backgroundOpacity { vtbackend::Opacity(
0xFF) };
Expand Down
4 changes: 4 additions & 0 deletions src/contour/ConfigDocumentation.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ constexpr StringLiteral StatusDisplayPosition {
"\n"
};

constexpr StringLiteral IndicatorStatusLineLeft { "left: \"{}\"\n" };
constexpr StringLiteral IndicatorStatusLineMiddle { "middle: \"{}\"\n" };
constexpr StringLiteral IndicatorStatusLineRight { "right: \"{}\"\n" };

constexpr StringLiteral SyncWindowTitleWithHostWritableStatusDisplay {
"{comment} Synchronize the window title with the Host Writable status_line if\n"
"{comment} and only if the host writable status line was denied to be shown.\n"
Expand Down
3 changes: 3 additions & 0 deletions src/contour/TerminalSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ namespace
settings.maxImageRegisterCount = config.maxImageColorRegisters.value();
settings.statusDisplayType = profile.initialStatusDisplayType.value();
settings.statusDisplayPosition = profile.statusDisplayPosition.value();
settings.indicatorStatusLine.left = profile.indicatorStatusLineLeft.value();
settings.indicatorStatusLine.middle = profile.indicatorStatusLineMiddle.value();
settings.indicatorStatusLine.right = profile.indicatorStatusLineRight.value();
settings.syncWindowTitleWithHostWritableStatusDisplay =
profile.syncWindowTitleWithHostWritableStatusDisplay.value();
if (auto const* p = preferredColorPalette(profile.colors.value(), colorPreference))
Expand Down
2 changes: 2 additions & 0 deletions src/crispy/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ set(crispy_SOURCES
escape.h
file_descriptor.h
flags.h
interpolated_string.cpp interpolated_string.h
logstore.cpp logstore.h
overloaded.h
reference.h
Expand Down Expand Up @@ -129,6 +130,7 @@ if(CRISPY_TESTING)
TrieMap_test.cpp
base64_test.cpp
compose_test.cpp
interpolated_string_test.cpp
utils_test.cpp
result_test.cpp
ring_test.cpp
Expand Down
9 changes: 9 additions & 0 deletions src/crispy/flags.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ class flags
return flags<flag_type>::from_value(_value | static_cast<value_type>(other));
}

[[nodiscard]] auto reduce(auto init, auto f) const
{
auto result = std::move(init);
for (auto i = 0u; i < sizeof(flag_type) * 8; ++i)
if (auto const flag = static_cast<flag_type>(1 << i); test(flag))
result = f(std::move(result), flag);
return result;
}

private:
value_type _value = 0;
};
Expand Down
96 changes: 96 additions & 0 deletions src/crispy/interpolated_string.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: Apache-2.0
#include <crispy/interpolated_string.h>

namespace crispy
{

namespace
{
void parse_attribute(string_interpolation* interpolation, std::string_view attribute)
{
auto const equal = attribute.find('=');
if (equal != std::string_view::npos)
{
auto const key = attribute.substr(0, equal);
auto const value = attribute.substr(equal + 1);
interpolation->attributes[key] = value;
}
else
{
interpolation->flags.insert(attribute);
}
}
} // anonymous namespace

string_interpolation parse_interpolation(std::string_view text)
{
auto result = string_interpolation {};
auto const colon = text.find(':');
if (colon != std::string_view::npos)
{
result.name = text.substr(0, colon);
auto const attributes = text.substr(colon + 1);
size_t pos = 0;
while (pos < attributes.size())
{
auto const comma = attributes.find(',', pos);
if (comma == std::string_view::npos)
{
parse_attribute(&result, attributes.substr(pos));
break;
}
else
{
parse_attribute(&result, attributes.substr(pos, comma - pos));
pos = comma + 1;
}
}
}
else
{
result.name = text;
}
return result;
}

interpolated_string parse_interpolated_string(std::string_view text)
{
// "< {Clock:Bold,Italic,Color=#FFFF00} | {VTType} | {InputMode} {Search:Bold,Color=Yellow} >"

auto fragments = interpolated_string {};

size_t pos = 0;
while (pos < text.size())
{
auto const openBrace = text.find('{', pos);
if (openBrace == std::string_view::npos)
{
// no more open braces found, so we're done.
fragments.emplace_back(text.substr(pos));
return fragments;
}

if (auto const textFragment = text.substr(pos, openBrace - pos); !textFragment.empty())
// add text fragment before the open brace
fragments.emplace_back(textFragment);

auto const closeBrace = text.find('}', openBrace);
if (closeBrace == std::string_view::npos)
{
// no matching close brace found, so we're done.
fragments.emplace_back(parse_interpolation(text.substr(openBrace)));
return fragments;
}
else
{
// add interpolation fragment
auto const fragment = text.substr(openBrace + 1, closeBrace - openBrace - 1);
fragments.emplace_back(parse_interpolation(fragment));
pos = closeBrace + 1;
}
}

return fragments;
}

} // namespace crispy
33 changes: 33 additions & 0 deletions src/crispy/interpolated_string.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once

#include <map>
#include <set>
#include <string_view>
#include <variant>
#include <vector>

namespace crispy
{

struct string_interpolation
{
std::string_view name;
std::set<std::string_view> flags;
std::map<std::string_view, std::string_view> attributes;

bool operator==(string_interpolation const& rhs) const noexcept
{
return name == rhs.name && flags == rhs.flags && attributes == rhs.attributes;
}

bool operator!=(string_interpolation const& rhs) const noexcept { return !(*this == rhs); }
};

using interpolated_string_fragment = std::variant<string_interpolation, std::string_view>;
using interpolated_string = std::vector<interpolated_string_fragment>;

string_interpolation parse_interpolation(std::string_view text);
interpolated_string parse_interpolated_string(std::string_view text);

} // namespace crispy
38 changes: 38 additions & 0 deletions src/crispy/interpolated_string_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include <crispy/interpolated_string.h>

#include <fmt/format.h>

#include <catch2/catch_test_macros.hpp>

TEST_CASE("interpolated_string.parse_interpolation")
{
using crispy::parse_interpolation;

auto const interpolation = parse_interpolation("Clock:Bold,Italic,Color=#FFFF00");
CHECK(interpolation.name == "Clock");
CHECK(interpolation.flags.size() == 2);
CHECK(interpolation.flags.count("Bold"));
CHECK(interpolation.flags.count("Italic") == 1);
CHECK(interpolation.attributes.size() == 1);
CHECK(interpolation.attributes.count("Color"));
CHECK(interpolation.attributes.at("Color") == "#FFFF00");
}

TEST_CASE("interpolated_string.parse_interpolated_string")
{
using crispy::parse_interpolated_string;

auto const interpolated = parse_interpolated_string("< {Clock:Bold,Italic,Color=#FFFF00} | {VTType}");

CHECK(interpolated.size() == 4);

REQUIRE(std::holds_alternative<std::string_view>(interpolated[0]));
REQUIRE(std::get<std::string_view>(interpolated[0]) == "< ");

REQUIRE(std::holds_alternative<crispy::string_interpolation>(interpolated[1]));

REQUIRE(std::holds_alternative<std::string_view>(interpolated[2]));
REQUIRE(std::get<std::string_view>(interpolated[2]) == " | ");

REQUIRE(std::holds_alternative<crispy::string_interpolation>(interpolated[3]));
}
2 changes: 2 additions & 0 deletions src/vtbackend/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ set(vtbackend_HEADERS
Sequence.h
Sequencer.h
SixelParser.h
StatusLineBuilder.h
Terminal.h
VTType.h
VTWriter.h
Expand Down Expand Up @@ -69,6 +70,7 @@ set(vtbackend_SOURCES
Sequence.cpp
Sequencer.cpp
SixelParser.cpp
StatusLineBuilder.cpp
Terminal.cpp
TerminalState.cpp
VTType.cpp
Expand Down
Loading

0 comments on commit fca59ef

Please sign in to comment.