From ccf8be8ade3aeecf7b0d361f245a4e53205b85ef Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sat, 12 Apr 2025 13:28:19 -0700 Subject: [PATCH 01/19] terminal: fix damage tracking when insert/deleting characters --- lib/src/terminal/screen.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/src/terminal/screen.cpp b/lib/src/terminal/screen.cpp index cd793ea..3545efe 100644 --- a/lib/src/terminal/screen.cpp +++ b/lib/src/terminal/screen.cpp @@ -321,6 +321,11 @@ void Screen::insert_blank_characters(u32 count) { ASSERT(text_start); row.text.erase(text_start.value(), row.text.end()); + // Mark any cells which have moved as dirty. + for (auto& cell : row.cells | di::drop(m_cursor.col)) { + cell.stale = false; + } + // Finally, insert the blank cells. Note that to implement bce this would need to // preserve the background color. The cursor position is unchanged, as is // the cursor byte offset. @@ -381,6 +386,11 @@ void Screen::delete_characters(u32 count) { ASSERT(text_end); row.text.erase(text_start.value(), text_end.value()); + // Mark any cells which have moved as dirty. + for (auto& cell : row.cells | di::drop(m_cursor.col + max_to_delete)) { + cell.stale = false; + } + // Insert blank cells at the end of the row. Note that to implement bce this would need to // preserve the background color. The cursor position is unchanged, as is the cursor byte offset. row.cells.resize(max_width()); From 6d560d1b09ab2d669a69a54bf1c627f531353566 Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sat, 12 Apr 2025 13:55:06 -0700 Subject: [PATCH 02/19] meta: add fzf runtime as a dependency The plan is to implement menus and other UI components as a popup terminal running fzf. I think this will super cool. --- README.md | 5 +++-- docs/pages/build.md | 9 ++++++++- docs/pages/install.md | 5 +++-- meta/nix/packages.nix | 38 ++++++++++++++++++++++++++++++++++---- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2850eae..8fc6848 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,9 @@ details. ## Dependencies -This project depends on the [dius](https://github.com/coletrammer/dius) library, to use its cross-platform -abstractions. +- [fzf](https://github.com/junegunn/fzf) program, for various popup memus +- [dius](https://github.com/coletrammer/dius) library, to use its cross-platform + abstractions. ## Installing diff --git a/docs/pages/build.md b/docs/pages/build.md index f7bdde5..64f66f9 100644 --- a/docs/pages/build.md +++ b/docs/pages/build.md @@ -11,6 +11,8 @@ This package can be built either directly through [CMake](https://cmake.org/) or ### Dependencies +- The [fzf](https://github.com/junegunn/fzf) program. This is used at runtime for various menus. To be clear, this + dependency is only needed for the `ttx` application and not the terminal emulation library itself. - The [dius](https://github.com/coletrammer/dius) library. - The [di](https://github.com/coletrammer/di) library (dependency of dius). @@ -66,6 +68,9 @@ Afterwards, use the library via `target_link_libraries(target PRIVATE ttx::ttx)` ### Note to packagers +Any `ttx` package should depend on `fzf` as `ttx` requires the `fzf` program to be +in the user's PATH on application startup. + The `CMAKE_INSTALL_INCLUDEDIR` is set to a path other than just `include` if the project is configured as a top level project to avoid indirectly including other libraries when installed to a common prefix. Please review the @@ -98,7 +103,9 @@ ttx = { Then include `inputs.ttx.packages.${system}.ttx-lib` in the `buildInputs` of your derivation. Assuming your project is using CMake, `find_package(ttx)` will succeed automatically. -This flake provides takes the di and dius libraries as a flake input, so it can be overridden easily. +This flake provides takes the di and dius libraries as a flake input, so it can be overridden easily. The +`fzf` dependency is taken from nixpkgs (which is also easy to override), and the `ttx` binary is wrapped +by nix to ensure `fzf` will be in the `PATH` variable when running `ttx`. ### Manual Build Commands diff --git a/docs/pages/install.md b/docs/pages/install.md index 20b7fca..d2f577d 100644 --- a/docs/pages/install.md +++ b/docs/pages/install.md @@ -1,8 +1,9 @@ # Install The easiest way to install ttx is using [nix](https://nixos). As of now, the only alternative is compiling from source. -In the future, a statically linked binary will available via a GitHub release. To build ttx from source, -see the steps [here](./build.md). +In the future, a statically linked binary will available via a GitHub release. However the user will +still need to install `fzf` to make use to the application. To build ttx from source, see the steps +[here](./build.md). ## Installing with Nix diff --git a/meta/nix/packages.nix b/meta/nix/packages.nix index 0c77d8c..ad42406 100644 --- a/meta/nix/packages.nix +++ b/meta/nix/packages.nix @@ -1,4 +1,4 @@ -{ inputs, ... }: +{ inputs, lib, ... }: { perSystem = { pkgs, system, ... }: @@ -24,7 +24,7 @@ ]; }; - mkAppPackage = + mkUnwrappedPackage = stdenv: name: dep: stdenv.mkDerivation { name = "${name}-${version}"; @@ -41,6 +41,30 @@ buildInputs = [ dep ]; }; + mkWrappedPackage = + stdenv: name: dep: fzf: + let + runtimeDeps = [ fzf ]; + in + stdenv.mkDerivation { + name = "${name}-${version}"; + version = version; + + nativeBuildInputs = with pkgs; [ + makeBinaryWrapper + ]; + + unpackPhase = "true"; + installPhase = '' + mkdir -p $out/bin + cp ${dep}/bin/* $out/bin + ''; + postFixup = '' + wrapProgram $out/bin/ttx \ + --suffix PATH : ${lib.makeBinPath runtimeDeps} + ''; + }; + mkApp = pkg: { type = "app"; program = "${pkg}/bin/ttx"; @@ -48,8 +72,14 @@ ttx-lib = mkLibPackage pkgs.stdenv "ttx-lib" "dius"; ttx-lib-dius-runtime = mkLibPackage pkgs.stdenv "ttx-lib-dius-runtime" "dius-runtime"; - ttx = mkAppPackage pkgs.stdenv "ttx" ttx-lib; - ttx-dius-runtime = mkAppPackage pkgs.stdenv "ttx-dius-runtime" ttx-lib-dius-runtime; + ttx-unwrapped = mkUnwrappedPackage pkgs.stdenv "ttx-unwrapped" ttx-lib; + ttx-dius-runtime-unwrapped = + mkUnwrappedPackage pkgs.stdenv "ttx-dius-runtime-unwrapped" + ttx-lib-dius-runtime; + ttx = mkWrappedPackage pkgs.stdenv "ttx" ttx-unwrapped pkgs.fzf; + ttx-dius-runtime = + mkWrappedPackage pkgs.stdenv "ttx-dius-runtime" ttx-dius-runtime-unwrapped + pkgs.fzf; ttx-app = mkApp ttx; ttx-app-dius-runtime = mkApp ttx-dius-runtime; From 43c9d940a80f0a6d91341fd6034cdf5a3374e576 Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sat, 12 Apr 2025 14:39:35 -0700 Subject: [PATCH 03/19] client: support full screen panes Now, prefix+z will toggle full-screen on the active pane. In full- screen mode, the only way to navigate to other panes it to disable the full screen mode. --- src/actions.cpp | 20 ++++++++++++++++++ src/actions.h | 1 + src/key_bind.cpp | 10 +++++++++ src/layout_state.cpp | 15 ++++++++++---- src/layout_state.h | 1 + src/render.cpp | 19 +++++++++++------ src/tab.cpp | 49 +++++++++++++++++++++++++++++++++++++++----- src/tab.h | 17 +++++++++++---- 8 files changed, 113 insertions(+), 19 deletions(-) diff --git a/src/actions.cpp b/src/actions.cpp index bc99dbb..2c7abb8 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -152,6 +152,26 @@ auto exit_pane() -> Action { }; } +auto toggle_full_screen_pane() -> Action { + return { + .description = "Toggle full screen for the active pane"_s, + .apply = + [](ActionContext const& context) { + context.layout_state.with_lock([&](LayoutState& state) { + for (auto& pane : state.active_pane()) { + auto& tab = *state.active_tab(); + if (&pane == state.full_screen_pane().data()) { + tab.set_full_screen_pane(nullptr); + } else { + tab.set_full_screen_pane(&pane); + } + } + }); + context.render_thread.request_render(); + }, + }; +} + auto add_pane(Direction direction) -> Action { return { .description = *di::present("Add a new pane, in a {} position relative to the active pane"_sv, direction), diff --git a/src/actions.h b/src/actions.h index 994ed20..49cea1a 100644 --- a/src/actions.h +++ b/src/actions.h @@ -15,6 +15,7 @@ auto quit() -> Action; auto save_state(di::Path path) -> Action; auto stop_capture() -> Action; auto exit_pane() -> Action; +auto toggle_full_screen_pane() -> Action; auto add_pane(Direction direction) -> Action; auto scroll(Direction direction, i32 amount_in_cells) -> Action; auto send_to_pane() -> Action; diff --git a/src/key_bind.cpp b/src/key_bind.cpp index 7f4b5e5..100be74 100644 --- a/src/key_bind.cpp +++ b/src/key_bind.cpp @@ -94,6 +94,11 @@ static auto make_replay_key_binds() -> di::Vector { .mode = InputMode::Insert, .action = scroll(Direction::Horizontal, -1), }); + result.push_back({ + .key = Key::Z, + .mode = InputMode::Insert, + .action = toggle_full_screen_pane(), + }); make_navigate_binds(result, InputMode::Insert, InputMode::Insert); return result; @@ -160,6 +165,11 @@ auto make_key_binds(Key prefix, di::Path save_state_path, bool replay_mode) -> d .mode = InputMode::Normal, .action = exit_pane(), }); + result.push_back({ + .key = Key::Z, + .mode = InputMode::Normal, + .action = toggle_full_screen_pane(), + }); result.push_back({ .key = Key::BackSlash, .modifiers = Modifiers::Shift, diff --git a/src/layout_state.cpp b/src/layout_state.cpp index 695c9cb..8b50cc9 100644 --- a/src/layout_state.cpp +++ b/src/layout_state.cpp @@ -15,9 +15,9 @@ void LayoutState::layout(di::Optional size) { } if (hide_status_bar()) { // Now status bar when forcing a single pane. - m_active_tab->layout(m_size, 0, 0); + m_active_tab->layout(m_size); } else { - m_active_tab->layout(m_size.rows_shrinked(1), 1, 0); + m_active_tab->layout(m_size.rows_shrinked(1)); } } @@ -82,9 +82,9 @@ auto LayoutState::remove_pane(Tab& tab, Pane* pane) -> di::Box { auto LayoutState::add_pane(Tab& tab, CreatePaneArgs args, Direction direction, RenderThread& render_thread) -> di::Result<> { if (hide_status_bar()) { - return tab.add_pane(m_next_pane_id++, m_size, 0, 0, di::move(args), direction, render_thread); + return tab.add_pane(m_next_pane_id++, m_size, di::move(args), direction, render_thread); } - return tab.add_pane(m_next_pane_id++, m_size.rows_shrinked(1), 1, 0, di::move(args), direction, render_thread); + return tab.add_pane(m_next_pane_id++, m_size.rows_shrinked(1), di::move(args), direction, render_thread); } auto LayoutState::add_tab(CreatePaneArgs args, RenderThread& render_thread) -> di::Result<> { @@ -107,4 +107,11 @@ auto LayoutState::active_pane() const -> di::Optional { } return active_tab()->active(); } + +auto LayoutState::full_screen_pane() const -> di::Optional { + if (!active_tab()) { + return {}; + } + return active_tab()->full_screen_pane(); +} } diff --git a/src/layout_state.h b/src/layout_state.h index 6404e66..530c5a1 100644 --- a/src/layout_state.h +++ b/src/layout_state.h @@ -27,6 +27,7 @@ class LayoutState { } auto active_pane() const -> di::Optional; + auto full_screen_pane() const -> di::Optional; auto size() const -> Size { return m_size; } auto hide_status_bar() const -> bool { return m_hide_status_bar; } diff --git a/src/render.cpp b/src/render.cpp index ef9876b..5a644d3 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -106,6 +106,7 @@ struct Render { di::Optional& cursor; Tab& tab; LayoutState& state; + bool have_status_bar { false }; void operator()(di::Box const& node) { (*this)(*node); } @@ -117,14 +118,14 @@ struct Render { auto [row, col, size] = di::visit(PositionAndSize {}, child); renderer.set_bound(0, 0, state.size().cols, state.size().rows); if (node.direction == Direction::Horizontal) { - for (auto r : di::range(row, row + size.rows)) { + for (auto r : di::range(row + have_status_bar, row + have_status_bar + size.rows)) { auto code_point = U'│'; renderer.put_text(code_point, r, col - 1); } } else if (node.direction == Direction::Vertical) { for (auto c : di::range(col, col + size.cols)) { auto code_point = U'─'; - renderer.put_text(code_point, row - 1, c); + renderer.put_text(code_point, row + have_status_bar - 1, c); } } } @@ -135,9 +136,10 @@ struct Render { } void operator()(LayoutEntry const& entry) { - renderer.set_bound(entry.row, entry.col, entry.size.cols, entry.size.rows); + renderer.set_bound(entry.row + have_status_bar, entry.col, entry.size.cols, entry.size.rows); auto pane_cursor = entry.pane->draw(renderer); if (entry.pane == tab.active().data()) { + pane_cursor.cursor_row += have_status_bar; pane_cursor.cursor_row += entry.row; pane_cursor.cursor_col += entry.col; cursor = pane_cursor; @@ -164,8 +166,13 @@ void RenderThread::do_render(Renderer& renderer) { // Status bar. if (!state.hide_status_bar()) { auto text = di::enumerate(state.tabs()) | di::transform(di::uncurry([&](usize i, di::Box const& tab) { - return *di::present("[{}{} {}]"_sv, tab.get() == active_tab.data() ? U'*' : U' ', i + 1, - tab->name()); + auto sign = U' '; + if (tab->full_screen_pane()) { + sign = U'+'; + } else if (tab.get() == active_tab.data()) { + sign = U'*'; + } + return *di::present("[{}{} {}]"_sv, sign, i + 1, tab->name()); })) | di::join_with(U' ') | di::to(); renderer.clear_row(0); @@ -178,7 +185,7 @@ void RenderThread::do_render(Renderer& renderer) { auto cursor = di::Optional {}; - Render(renderer, cursor, tab, state)(*tree); + Render(renderer, cursor, tab, state, !state.hide_status_bar())(*tree); (void) renderer.finish(dius::stdin, cursor.value_or({ .hidden = true })); }); diff --git a/src/tab.cpp b/src/tab.cpp index a030222..3c8fabf 100644 --- a/src/tab.cpp +++ b/src/tab.cpp @@ -3,11 +3,22 @@ #include "di/serialization/base64.h" #include "dius/print.h" #include "render.h" +#include "ttx/direction.h" namespace ttx { -void Tab::layout(Size const& size, u32 row, u32 col) { +void Tab::layout(Size const& size) { m_size = size; - m_layout_tree = m_layout_root.layout(size, row, col); + + if (m_full_screen_pane) { + // In full screen mode, circumvent ordinary layout. + m_full_screen_pane->resize(m_size); + m_layout_tree = + di::make_box(0, 0, size, di::Vector, LayoutEntry>> {}, nullptr, + &m_layout_root, Direction::None); + m_layout_tree->children.emplace_back(LayoutEntry { 0, 0, size, m_layout_tree.get(), m_full_screen_pane }); + } else { + m_layout_tree = m_layout_root.layout(size, 0, 0); + } invalidate_all(); } @@ -18,6 +29,11 @@ void Tab::invalidate_all() { } auto Tab::remove_pane(Pane* pane) -> di::Box { + // Clear full screen pane. The caller makes sure to call layout() for us. + if (m_full_screen_pane == pane) { + m_full_screen_pane = nullptr; + } + // Clear active pane. if (m_active == pane) { if (pane) { @@ -32,9 +48,9 @@ auto Tab::remove_pane(Pane* pane) -> di::Box { return m_layout_root.remove_pane(pane); } -auto Tab::add_pane(u64 pane_id, Size const& size, u32 row, u32 col, CreatePaneArgs args, Direction direction, - RenderThread& render_thread) -> di::Result<> { - auto [new_layout, pane_layout, pane_out] = m_layout_root.split(size, row, col, m_active, direction); +auto Tab::add_pane(u64 pane_id, Size const& size, CreatePaneArgs args, Direction direction, RenderThread& render_thread) + -> di::Result<> { + auto [new_layout, pane_layout, pane_out] = m_layout_root.split(size, 0, 0, m_active, direction); if (!pane_layout || !pane_out || pane_layout->size == Size {}) { // NOTE: this happens when the visible terminal size is too small. @@ -128,11 +144,34 @@ void Tab::navigate(NavigateDirection direction) { } } +auto Tab::set_full_screen_pane(Pane* pane) -> bool { + if (m_full_screen_pane == pane) { + return false; + } + + if (pane == nullptr) { + m_full_screen_pane = nullptr; + layout(m_size); + return true; + } + + m_full_screen_pane = pane; + set_active(pane); + layout(m_size); + return true; +} + auto Tab::set_active(Pane* pane) -> bool { if (m_active == pane) { return false; } + // Clear full screen pane, if said pane is no longer focused. + if (m_full_screen_pane && m_full_screen_pane != pane) { + m_full_screen_pane = nullptr; + layout(m_size); + } + // Unfocus the old pane, and focus the new pane. if (is_active() && m_active) { m_active->event(FocusEvent::focus_out()); diff --git a/src/tab.h b/src/tab.h index 04a9658..bd03b1f 100644 --- a/src/tab.h +++ b/src/tab.h @@ -26,14 +26,14 @@ struct Tab { public: explicit Tab(di::String name) : m_name(di::move(name)) {} - void layout(Size const& size, u32 row, u32 col); + void layout(Size const& size); void invalidate_all(); // Returns the removed pane, if found. auto remove_pane(Pane* pane) -> di::Box; - auto add_pane(u64 pane_id, Size const& size, u32 row, u32 col, CreatePaneArgs args, Direction direction, - RenderThread& render_thread) -> di::Result<>; + auto add_pane(u64 pane_id, Size const& size, CreatePaneArgs args, Direction direction, RenderThread& render_thread) + -> di::Result<>; void navigate(NavigateDirection direction); @@ -61,7 +61,15 @@ struct Tab { auto panes() const -> di::Ring const& { return m_panes_ordered_by_recency; } auto set_is_active(bool b) -> bool; - auto is_active() -> bool { return m_is_active; } + auto is_active() const -> bool { return m_is_active; } + + auto full_screen_pane() const -> di::Optional { + if (!m_full_screen_pane) { + return {}; + } + return *m_full_screen_pane; + } + auto set_full_screen_pane(Pane* pane) -> bool; private: Size m_size; @@ -71,5 +79,6 @@ struct Tab { di::Ring m_panes_ordered_by_recency {}; bool m_is_active { false }; Pane* m_active { nullptr }; + Pane* m_full_screen_pane { nullptr }; }; } From 880cc59fac9256d2a2abf3a4627e14e42a24af0a Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sat, 12 Apr 2025 15:04:50 -0700 Subject: [PATCH 04/19] terminal+client: implement DECSTR soft reset We mostly want this to recover the terminal when a full-screen application crashes before cleaning up the terminal state. This happens often when running ttx inside ttx. Now, pressing prefix+shift+r will soft reset the terminal. Additionally, this improves conformance on the hyperlink test as soft reset was previously ignored, and thus didn't reset current hyperlink. --- lib/include/ttx/pane.h | 1 + lib/include/ttx/terminal.h | 3 + lib/src/pane.cpp | 6 + lib/src/terminal.cpp | 51 +++++++ lib/test/cases/hyperlink-demo.expected.ansi | 160 ++++++++------------ src/actions.cpp | 15 ++ src/actions.h | 1 + src/key_bind.cpp | 6 + 8 files changed, 145 insertions(+), 98 deletions(-) diff --git a/lib/include/ttx/pane.h b/lib/include/ttx/pane.h index 6255e87..502de80 100644 --- a/lib/include/ttx/pane.h +++ b/lib/include/ttx/pane.h @@ -67,6 +67,7 @@ class Pane { void scroll(Direction direction, i32 amount_in_cells); auto save_state(di::PathView path) -> di::Result<>; void stop_capture(); + void soft_reset(); void exit(); private: diff --git a/lib/include/ttx/terminal.h b/lib/include/ttx/terminal.h index 6bc2cc5..4e6f011 100644 --- a/lib/include/ttx/terminal.h +++ b/lib/include/ttx/terminal.h @@ -76,6 +76,8 @@ class Terminal { auto bracked_paste_mode() const -> BracketedPasteMode { return m_bracketed_paste_mode; } + void soft_reset(); + void invalidate_all(); auto outgoing_events() -> di::Vector { return di::move(m_outgoing_events); } @@ -160,6 +162,7 @@ class Terminal { void csi_decstbm(Params const& params); void csi_scosc(Params const& params); void csi_scorc(Params const& params); + void csi_decstr(Params const& params); void csi_xtwinops(Params const& params); void csi_set_key_reporting_flags(Params const& params); diff --git a/lib/src/pane.cpp b/lib/src/pane.cpp index f10ebc7..66c0637 100644 --- a/lib/src/pane.cpp +++ b/lib/src/pane.cpp @@ -485,6 +485,12 @@ void Pane::stop_capture() { m_capture.store(false, di::MemoryOrder::Release); } +void Pane::soft_reset() { + m_terminal.with_lock([&](Terminal& terminal) { + terminal.soft_reset(); + }); +} + void Pane::exit() { (void) m_process.signal(dius::Signal::Hangup); } diff --git a/lib/src/terminal.cpp b/lib/src/terminal.cpp index b1623db..07ef2a6 100644 --- a/lib/src/terminal.cpp +++ b/lib/src/terminal.cpp @@ -14,6 +14,7 @@ #include "ttx/params.h" #include "ttx/paste_event_io.h" #include "ttx/terminal/escapes/osc_8.h" +#include "ttx/terminal/screen.h" namespace ttx { Terminal::Terminal(u64 id, dius::SyncFile& psuedo_terminal, Size const& size) @@ -181,6 +182,17 @@ void Terminal::on_parser_result(CSI const& csi) { } } + if (csi.intermediate == "!"_sv) { + switch (csi.terminator) { + case 'p': { + csi_decstr(csi.params); + return; + } + default: + return; + } + } + if (!csi.intermediate.empty()) { return; } @@ -978,6 +990,12 @@ void Terminal::csi_scorc(Params const&) { esc_decrc(); } +// Soft Terminal Reset - https://vt100.net/docs/vt510-rm/DECSTR.html +void Terminal::csi_decstr(Params const&) { + // NOTE: this isn't exactly standards compliant. + soft_reset(); +} + // Window manipulation - // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-Ps;Ps;Ps-t.1EB0 void Terminal::csi_xtwinops(Params const& params) { @@ -1147,6 +1165,39 @@ auto Terminal::active_screen() const -> ScreenState const& { return m_primary_screen; } +void Terminal::soft_reset() { + // The goal of this routine is to make the terminal usable again after a full-screen + // application crashes without cleaning anything up. This won't fully follow the + // spec for DECSTR (https://vt100.net/docs/vt510-rm/DECSTR.html), becuase it claims + // autowrap should be disabled. Autowrap is on by default in this terminal. + + set_use_alternate_screen_buffer(false); + + active_screen().cursor_style = CursorStyle::SteadyBlock; + active_screen().saved_cursor = {}; + + auto& screen = active_screen().screen; + auto cursor = screen.save_cursor(); + csi_decstbm({}); + screen.set_current_graphics_rendition({}); + screen.set_current_hyperlink({}); + cursor.origin_mode = terminal::OriginMode::Disabled; + screen.restore_cursor(cursor); + screen.invalidate_all(); + + m_allow_80_132_col_mode = false; + m_allow_force_terminal_size = false; + m_auto_wrap_mode = terminal::AutoWrapMode::Enabled; + m_mouse_encoding = MouseEncoding::X10; + m_mouse_protocol = MouseProtocol::None; + m_key_reporting_flags_stack.clear(); + m_key_reporting_flags = KeyReportingFlags::None; + m_cursor_hidden = false; + m_disable_drawing = false; + + resize(visible_size()); +} + auto Terminal::state_as_escape_sequences() const -> di::String { auto writer = di::VectorWriter<> {}; diff --git a/lib/test/cases/hyperlink-demo.expected.ansi b/lib/test/cases/hyperlink-demo.expected.ansi index 31eb99a..e3e9814 100644 --- a/lib/test/cases/hyperlink-demo.expected.ansi +++ b/lib/test/cases/hyperlink-demo.expected.ansi @@ -1,4 +1,4 @@ -c[?7hTests for ]8;id=1;https://bugzilla.gnome.org/show_bug.cgi?id=779734\gnome-terminal #779734]8;;\ and ]8;id=2;https://gitlab.com/gnachman/iterm2/issues/5158\iTerm2 #5158 +c[?7hTests for ]8;id=1;https://bugzilla.gnome.org/show_bug.cgi?id=779734\gnome-terminal #779734]8;;\ and ]8;id=2;https://gitlab.com/gnachman/iterm2/issues/5158\iTerm2 #5158 ]8;;\═════════════════════════════════════════════════  commit ]8;id=3;https://git.gnome.org/browse/vte/commit/?id=a9b0b4c75a6dc7282f7cfcaef71413d69f7f0731\a9b0b4c75a6dc7282f7cfcaef71413d69f7f0731 @@ -12,6 +12,42 @@ Date: Sat Oct 24 00:12:22 2015 +0200 ]8;;\commit ]8;id=6;https://git.gnome.org/browse/vte/commit/?id=6a74baeaabb0a1ce54444611b324338f94721a5c\6a74baeaabb0a1ce54444611b324338f94721a5c ]8;;\Merge: ]8;id=7;https://git.gnome.org/browse/vte/commit/?id=3fac4469de267f662c761ea4f247c8017ced483d\3fac446]8;;\ ]8;id=8;https://git.gnome.org/browse/vte/commit/?id=56ea5810759b9943a4203f9382919f058a66f224\56ea581 ]8;;\Author: Christian Persch <]8;id=9;mailto:chpe@gnome.org\chpe@gnome.org]8;;\> +Date: Mon Apr 27 13:48:52 2015 +0200 + + Merge branch 'work-html' into merge-html + +]8;id=10;file:///var/lib/gconf/defaults/%25gconf-tree.xml\A file with a % sign in its name (escaped as %25) +]8;;\Icons: ]8;id=11;file:///usr/share/icons/Adwaita/256x256/apps/preferences-desktop-theme.png\Theme]8;;\ ]8;id=12;file:///usr/share/icons/Adwaita/256x256/categories/applications-graphics.png\Graphics]8;;\ ]8;id=13;file:///usr/share/icons/Adwaita/256x256/status/starred.png\Star]8;;\ ]8;id=14;file:///usr/share/icons/Adwaita/256x256/actions/system-log-out.png\Exit]8;;\ ]8;id=15;file:///usr/share/icons/Adwaita/512x512/apps/utilities-terminal.png\Terminal +]8;;\Backgrounds: ]8;id=16;file:///usr/share/backgrounds/gnome/Bokeh_Tails.jpg\Bokeh]8;;\ ]8;id=17;file:///usr/share/backgrounds/gnome/Chmiri.jpg\Chmiri]8;;\ ]8;id=18;file:///usr/share/backgrounds/gnome/Dark_Ivy.jpg\Ivy]8;;\ ]8;id=19;file:///usr/share/backgrounds/gnome/Flowerbed.jpg\Flower]8;;\ ]8;id=20;file:///usr/share/backgrounds/gnome/Godafoss_Iceland.jpg\Iceland]8;;\ ]8;id=21;file:///usr/share/backgrounds/gnome/Icescape.jpg\Icescape]8;;\ ]8;id=22;file:///usr/share/backgrounds/gnome/Mirror.jpg\Mirror]8;;\ ]8;id=23;file:///usr/share/backgrounds/gnome/Road.jpg\Road]8;;\ ]8;id=24;file:///usr/share/backgrounds/gnome/Sandstone.jpg\Sandstone]8;;\ ]8;id=25;file:///usr/share/backgrounds/gnome/Stones.jpg\Ston + +]8;id=28;https://en.wikipedia.org/wiki/�\Wiki page of � (unescaped raw Latin-1; invalid UTF-8) +]8;id=29;https://en.wikipedia.org/wiki/Á\Wiki page of Á (unescaped raw UTF-8) +]8;id=30;https://en.wikipedia.org/wiki/%C3%81\Wiki page of Á (escaped as %C3%81) +]8;id=31;https://en.wikipedia.org/wiki/%25\Wiki page of % (escaped as %25) +]8;id=32;http://%d8%a7%d9%84%d9%85%d8%ba%d8%b1%d8%a8.icom.museum\http://المغرب.icom.museum (with URI-escaped domain name) +]8;id=33;http://xn--4wa8awb4637h.org\http://xn--4wa8awb4637h.org (Παν語.org) + +]8;;\Two adjacent links pointing to the same URL: ]8;id=34;http://example.com/foo\foo]8;id=35;http://example.com/foo\foo +]8;;\Two adjacent links pointing to different URLs: ]8;id=36;http://example.com/foo\foo]8;id=37;http://example.com/bar\bar + +]8;;\The same two without closing the first link: ]8;id=38;http://example.com/foo\foo]8;id=39;http://example.com/foo\foo]8;;\ ]8;id=40;http://example.com/foo\foo]8;id=41;http://example.com/bar\bar + +]8;;\A URL wrapping to the next line, and a trailing whitespace: ]8;id=42;http://example.com/foobar\foo +bar + +]8;id=43;http://example.com/colors\Multi-colour link also tests that "\e[m" or "\e[0m" does not terminate the link + +]8;;\Soft reset "\e[!p" resets attributes and terminates link: ]8;id=44;http://example.com/softreset\foo]8;;\bar + +]8;id=45;http://example.com/width\Some CJK and combining accents: 䀀䀁䀂ćĝm̃n̄o̅ + +]8;;\(Introducing the "under_score" character for even more fun) + +Explicit and implicit link: ]8;id=46;http://example.com/under_score\http://example.com/under_score +]8;;\Explicit and implicit link with different targets: ]8;id=47;http://example.com/explicit_under_score\http://example.com/implicit_under_score +]8;;\Explicit and implicit link, broken into two lines: ]8;id=48;http://example.com/under_score\http://examp +le.com/under_score + @@ -36,100 +72,28 @@ Date: Sat Oct 24 00:12:22 2015 +0200 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Date: Mon Apr 27 13:48:52 2015 +0200 - - Merge branch 'work-html' into merge-html - -]8;id=10;file:///var/lib/gconf/defaults/%25gconf-tree.xml\A file with a % sign in its name (escaped as %25) -]8;;\Icons: ]8;id=11;file:///usr/share/icons/Adwaita/256x256/apps/preferences-desktop-theme.png\Theme]8;;\ ]8;id=12;file:///usr/share/icons/Adwaita/256x256/categories/applications-graphics.png\Graphics]8;;\ ]8;id=13;file:///usr/share/icons/Adwaita/256x256/status/starred.png\Star]8;;\ ]8;id=14;file:///usr/share/icons/Adwaita/256x256/actions/system-log-out.png\Exit]8;;\ ]8;id=15;file:///usr/share/icons/Adwaita/512x512/apps/utilities-terminal.png\Terminal -]8;;\Backgrounds: ]8;id=16;file:///usr/share/backgrounds/gnome/Bokeh_Tails.jpg\Bokeh]8;;\ ]8;id=17;file:///usr/share/backgrounds/gnome/Chmiri.jpg\Chmiri]8;;\ ]8;id=18;file:///usr/share/backgrounds/gnome/Dark_Ivy.jpg\Ivy]8;;\ ]8;id=19;file:///usr/share/backgrounds/gnome/Flowerbed.jpg\Flower]8;;\ ]8;id=20;file:///usr/share/backgrounds/gnome/Godafoss_Iceland.jpg\Iceland]8;;\ ]8;id=21;file:///usr/share/backgrounds/gnome/Icescape.jpg\Icescape]8;;\ ]8;id=22;file:///usr/share/backgrounds/gnome/Mirror.jpg\Mirror]8;;\ ]8;id=23;file:///usr/share/backgrounds/gnome/Road.jpg\Road]8;;\ ]8;id=24;file:///usr/share/backgrounds/gnome/Sandstone.jpg\Sandstone]8;;\ ]8;id=25;file:///usr/share/backgrounds/gnome/Stones.jpg\Stones]8;;\ ]8;id=26;file:///usr/share/backgrounds/gnome/Waterfalls.jpg\Waterfalls]8;;\ ]8;id=27;file:///usr/share/backgrounds/gnome/Waves.jpg\Waves - -]8;id=28;https://en.wikipedia.org/wiki/�\Wiki page of � (unescaped raw Latin-1; invalid UTF-8) -]8;id=29;https://en.wikipedia.org/wiki/Á\Wiki page of Á (unescaped raw UTF-8) -]8;id=30;https://en.wikipedia.org/wiki/%C3%81\Wiki page of Á (escaped as %C3%81) -]8;id=31;https://en.wikipedia.org/wiki/%25\Wiki page of % (escaped as %25) -]8;id=32;http://%d8%a7%d9%84%d9%85%d8%ba%d8%b1%d8%a8.icom.museum\http://المغرب.icom.museum (with URI-escaped domain name) -]8;id=33;http://xn--4wa8awb4637h.org\http://xn--4wa8awb4637h.org (Παν語.org) - -]8;;\Two adjacent links pointing to the same URL: ]8;id=34;http://example.com/foo\foo]8;id=35;http://example.com/foo\foo -]8;;\Two adjacent links pointing to different URLs: ]8;id=36;http://example.com/foo\foo]8;id=37;http://example.com/bar\bar - -]8;;\The same two without closing the first link: ]8;id=38;http://example.com/foo\foo]8;id=39;http://example.com/foo\foo]8;;\ ]8;id=40;http://example.com/foo\foo]8;id=41;http://example.com/bar\bar - -]8;;\A URL wrapping to the next line, and a trailing whitespace: ]8;id=42;http://example.com/foobar\foo -bar  - -]8;id=43;http://example.com/colors\Multi-colour link also tests that "\e[m" or "\e[0m" does not terminate the link - -]8;;\Soft reset "\e[!p" resets attributes and terminates link: ]8;id=44;http://example.com/softreset\foobar - -]8;id=45;http://example.com/width\Some CJK and combining accents: 䀀䀁䀂ćĝm̃n̄o̅ - -]8;;\(Introducing the "under_score" character for even more fun) - -Explicit and implicit link: ]8;id=46;http://example.com/under_score\http://example.com/under_score -]8;;\Explicit and implicit link with different targets: ]8;id=47;http://example.com/explicit_under_score\http://example.com/implicit_under_score -]8;;\Explicit and implicit link, broken into two lines: ]8;id=48;http://example.com/under_score\http://examp -le.com/under_score - -]8;;\Explicitly underlined links ("\e[4m"): -Explicit link only: ]8;id=49;http://example.com/under_score\I'm an explicit link with under_score -]8;;\Implicit link only: http://example.com/under_score -Both: ]8;id=50;http://example.com/under_score\http://example.com/under_score - -]8;;\Conflicting explicit and implicit links: http://example.com/foobar-]8;id=51;http://example.com/explicit\explicit]8;;\-rest - -Invisible explicit link: «]8;id=52;http://example.com/invisible\Can you see me?]8;;\» -Invisible implicit link: «http://example.com/how_about_me» - -]8;id=53;asdfghjkl\Explicit link with stupid target - -]8;id=54;http://example.com/.........30........40........50........60........70........80........90.......100\URL of 100 bytes -]8;id=55;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200\URL of 200 bytes -]8;id=56;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500\URL of 500 bytes -]8;id=57;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000\URL of 1000 bytes -]8;id=58;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500\URL of 1500 bytes -]8;id=59;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000\URL of 2000 bytes -]8;id=60;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080...\URL of 2083 bytes -]8;;\URL of 2084 bytes - -]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230;http://example.com/id\ID of 250 bytes once,]8;;\ ]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230;http://example.com/id\twice -ID of 251 bytes once,]8;;\ ]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230;http://example.com/id\twice - -78]8;;\[6 q[=1;0u \ No newline at end of file +]8;;\Explicitly underlined links ("\e[4m"): +Explicit link only: ]8;id=49;http://example.com/under_score\I'm an explicit link with under_score +]8;;\Implicit link only: http://example.com/under_score +Both: ]8;id=50;http://example.com/under_score\http://example.com/under_score + +]8;;\Conflicting explicit and implicit links: http://example.com/foobar-]8;id=51;http://example.com/explicit\explicit]8;;\-rest + +Invisible explicit link: «]8;id=52;http://example.com/invisible\Can you see me?]8;;\» +Invisible implicit link: «http://example.com/how_about_me» + +]8;id=53;asdfghjkl\Explicit link with stupid target + +]8;id=54;http://example.com/.........30........40........50........60........70........80........90.......100\URL of 100 bytes +]8;id=55;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200\URL of 200 bytes +]8;id=56;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500\URL of 500 bytes +]8;id=57;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000\URL of 1000 bytes +]8;id=58;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500\URL of 1500 bytes +]8;id=59;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000\URL of 2000 bytes +]8;id=60;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080...\URL of 2083 bytes +]8;;\URL of 2084 bytes + +]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230;http://example.com/id\ID of 250 bytes once,]8;;\ ]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230;http://example.com/id\twice +ID of 251 bytes once,]8;;\ ]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230;http://example.com/id\twice + +78]8;;\[2 q[=1;0u \ No newline at end of file diff --git a/src/actions.cpp b/src/actions.cpp index 2c7abb8..d55aecb 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -152,6 +152,21 @@ auto exit_pane() -> Action { }; } +auto soft_reset() -> Action { + return { + .description = "Soft reset the active pane"_s, + .apply = + [](ActionContext const& context) { + context.layout_state.with_lock([&](LayoutState& state) { + if (auto pane = state.active_pane()) { + pane->soft_reset(); + } + }); + context.render_thread.request_render(); + }, + }; +} + auto toggle_full_screen_pane() -> Action { return { .description = "Toggle full screen for the active pane"_s, diff --git a/src/actions.h b/src/actions.h index 49cea1a..65854c8 100644 --- a/src/actions.h +++ b/src/actions.h @@ -15,6 +15,7 @@ auto quit() -> Action; auto save_state(di::Path path) -> Action; auto stop_capture() -> Action; auto exit_pane() -> Action; +auto soft_reset() -> Action; auto toggle_full_screen_pane() -> Action; auto add_pane(Direction direction) -> Action; auto scroll(Direction direction, i32 amount_in_cells) -> Action; diff --git a/src/key_bind.cpp b/src/key_bind.cpp index 100be74..ec2141f 100644 --- a/src/key_bind.cpp +++ b/src/key_bind.cpp @@ -148,6 +148,12 @@ auto make_key_binds(Key prefix, di::Path save_state_path, bool replay_mode) -> d .mode = InputMode::Normal, .action = quit(), }); + result.push_back({ + .key = Key::R, + .modifiers = Modifiers::Shift, + .mode = InputMode::Normal, + .action = soft_reset(), + }); result.push_back({ .key = Key::I, .modifiers = Modifiers::Shift, From 3899effacf0c55f3e8c7b00ce7004c0c7c40cd70 Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sat, 12 Apr 2025 23:42:40 -0700 Subject: [PATCH 05/19] client: implement support for a single popup tab Currently, there's no way to use this functionality, but soon we'll put a menu in the popup. This commit also fixes 2 issues which caused crashes: 1. Improper cursor movement when resizing the screen. 2. Failing to delete erased panes from the pane list if that pane wasn't already active. --- lib/include/ttx/popup.h | 27 ++++++++++ lib/include/ttx/terminal/screen.h | 2 +- lib/src/popup.cpp | 37 ++++++++++++++ lib/src/terminal/screen.cpp | 7 +++ lib/test/src/test_popup.cpp | 70 ++++++++++++++++++++++++++ src/input.cpp | 29 +++++++++-- src/layout_state.cpp | 8 +++ src/layout_state.h | 5 ++ src/render.cpp | 18 +++++-- src/tab.cpp | 84 +++++++++++++++++++++++-------- src/tab.h | 12 ++++- 11 files changed, 269 insertions(+), 30 deletions(-) create mode 100644 lib/include/ttx/popup.h create mode 100644 lib/src/popup.cpp create mode 100644 lib/test/src/test_popup.cpp diff --git a/lib/include/ttx/popup.h b/lib/include/ttx/popup.h new file mode 100644 index 0000000..4be5ad8 --- /dev/null +++ b/lib/include/ttx/popup.h @@ -0,0 +1,27 @@ +#pragma once + +#include "ttx/layout.h" +#include "ttx/size.h" + +namespace ttx { +enum class PopupAlignment { + Left, + Right, + Top, + Bottom, + Center, +}; + +struct PopupLayout { + PopupAlignment alignment { PopupAlignment::Center }; + i64 relative_width { max_layout_precision / 2 }; // 50% width default + i64 relative_height { max_layout_precision / 2 }; // 50% height default +}; + +struct Popup { + di::Box pane {}; + PopupLayout layout_config; + + auto layout(Size const& size) -> LayoutEntry; +}; +} diff --git a/lib/include/ttx/terminal/screen.h b/lib/include/ttx/terminal/screen.h index d225507..0fdba17 100644 --- a/lib/include/ttx/terminal/screen.h +++ b/lib/include/ttx/terminal/screen.h @@ -183,7 +183,7 @@ class Screen { // Screen state. RowGroup m_active_rows; - bool m_whole_screen_dirty { false }; + bool m_whole_screen_dirty { true }; // Scroll back ScrollBack m_scroll_back; diff --git a/lib/src/popup.cpp b/lib/src/popup.cpp new file mode 100644 index 0000000..37364cc --- /dev/null +++ b/lib/src/popup.cpp @@ -0,0 +1,37 @@ +#include "ttx/popup.h" + +#include "ttx/layout.h" +#include "ttx/size.h" + +namespace ttx { +auto Popup::layout(Size const& size) -> LayoutEntry { + auto cols = (di::Rational(layout_config.relative_width, max_layout_precision) * size.cols).round(); + auto rows = (di::Rational(layout_config.relative_height, max_layout_precision) * size.rows).round(); + cols = di::max(cols, 1_i64); + rows = di::max(rows, 1_i64); + + auto empty_rows = u32(di::max(size.rows - rows, 0_i64)); + auto empty_cols = u32(di::max(size.cols - cols, 0_i64)); + + auto layout_size = Size(rows, cols, size.xpixels / cols, size.ypixels / rows); + auto [r, c] = [&] -> di::Tuple { + switch (layout_config.alignment) { + case PopupAlignment::Left: + return { di::divide_round_up(empty_rows, 2u), 0 }; + case PopupAlignment::Right: + return { di::divide_round_up(empty_rows, 2u), empty_cols }; + case PopupAlignment::Top: + return { 0, di::divide_round_up(empty_cols, 2u) }; + case PopupAlignment::Bottom: + return { empty_rows, di::divide_round_up(empty_cols, 2u) }; + case PopupAlignment::Center: + return { di::divide_round_up(empty_rows, 2u), di::divide_round_up(empty_cols, 2u) }; + } + return { 0, 0 }; + }(); + if (pane) { + pane->resize(layout_size); + } + return LayoutEntry { r, c, layout_size, nullptr, pane.get() }; +} +} diff --git a/lib/src/terminal/screen.cpp b/lib/src/terminal/screen.cpp index 3545efe..9a4c80b 100644 --- a/lib/src/terminal/screen.cpp +++ b/lib/src/terminal/screen.cpp @@ -116,6 +116,13 @@ void Screen::resize(Size const& size) { m_cursor.row = di::min(m_cursor.row, size.rows - 1); m_cursor.col = di::min(m_cursor.col, size.cols - 1); + // Recompute the cursor text offset. + auto& row_object = rows()[m_cursor.row]; + m_cursor.text_offset = 0; + for (auto const& cell : row_object.cells | di::take(m_cursor.col)) { + m_cursor.text_offset += cell.text_size; + } + // TODO: optimize! invalidate_all(); } diff --git a/lib/test/src/test_popup.cpp b/lib/test/src/test_popup.cpp new file mode 100644 index 0000000..8b1d098 --- /dev/null +++ b/lib/test/src/test_popup.cpp @@ -0,0 +1,70 @@ +#include "di/test/prelude.h" +#include "ttx/layout.h" +#include "ttx/popup.h" +#include "ttx/size.h" + +namespace popup { +using namespace ttx; + +static auto alignments() { + auto size = Size { 50, 60, 600, 50000 }; + + struct Case { + PopupLayout input {}; + LayoutEntry expected {}; + }; + + auto make_input = [&](PopupAlignment alignment, i64 height_percent, i64 width_percent) { + return PopupLayout { + .alignment = alignment, + .relative_width = max_layout_precision / 100 * width_percent, + .relative_height = max_layout_precision / 100 * height_percent, + }; + }; + + auto make_entry = [&](u32 row, u32 col, u32 rows, u32 cols) { + return LayoutEntry { + .row = row, + .col = col, + .size = Size(rows, cols, size.xpixels / cols, size.ypixels / rows), + }; + }; + + auto cases = di::Array { + // Regular + Case { + .input = make_input(PopupAlignment::Center, 50, 50), + .expected = make_entry(13, 15, 25, 30), + }, + Case { + .input = make_input(PopupAlignment::Bottom, 50, 50), + .expected = make_entry(25, 15, 25, 30), + }, + Case { + .input = make_input(PopupAlignment::Top, 50, 50), + .expected = make_entry(0, 15, 25, 30), + }, + Case { + .input = make_input(PopupAlignment::Left, 50, 50), + .expected = make_entry(13, 0, 25, 30), + }, + Case { + .input = make_input(PopupAlignment::Right, 50, 50), + .expected = make_entry(13, 30, 25, 30), + }, + // Too small + Case { + .input = make_input(PopupAlignment::Center, 1, 1), + .expected = make_entry(25, 30, 1, 1), + }, + }; + + for (auto const& [input, expected] : cases) { + auto popup = Popup { nullptr, input }; + auto result = popup.layout(size); + ASSERT_EQ(result, expected); + } +}; + +TEST(popup, alignments) +} diff --git a/src/input.cpp b/src/input.cpp index 7942f77..4966a02 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -119,14 +119,37 @@ void InputThread::handle_event(MouseEvent const& event) { } auto& tab = *state.active_tab(); + auto ev = event.translate({ 0, u32(-!state.hide_status_bar()) }, state.size()); + + // Check if we're hitting any popup with the mouse. + auto row = event.position().in_cells().y(); + auto col = event.position().in_cells().x(); + for (auto entry : tab.popup_layout()) { + if (row >= entry.row && row < entry.row + entry.size.rows && col >= entry.col && + col < entry.col + entry.size.cols) { + if (ev.type() != MouseEventType::Move) { + tab.set_active(entry.pane); + } + if (entry.pane->event(ev.translate({ -entry.col, -entry.row }, state.size()))) { + m_render_thread.request_render(); + } + return; + } + } + // Check if the event interests with any pane. for (auto const& entry : - tab.layout_tree()->hit_test(event.position().in_cells().y(), event.position().in_cells().x())) { - if (event.type() != MouseEventType::Move) { + tab.layout_tree()->hit_test(ev.position().in_cells().y(), ev.position().in_cells().x())) { + if (ev.type() != MouseEventType::Move) { + // Set the pane the user just clicked on as active. tab.set_active(entry.pane); + // If we had a popup, exit it as the user clicked out. + for (auto popup_entry : tab.popup_layout()) { + popup_entry.pane->exit(); + } } if (entry.pane == tab.active().data()) { - if (entry.pane->event(event.translate({ -entry.col, -entry.row }, state.size()))) { + if (entry.pane->event(ev.translate({ -entry.col, -entry.row }, state.size()))) { m_render_thread.request_render(); } } diff --git a/src/layout_state.cpp b/src/layout_state.cpp index 8b50cc9..a907137 100644 --- a/src/layout_state.cpp +++ b/src/layout_state.cpp @@ -87,6 +87,14 @@ auto LayoutState::add_pane(Tab& tab, CreatePaneArgs args, Direction direction, R return tab.add_pane(m_next_pane_id++, m_size.rows_shrinked(1), di::move(args), direction, render_thread); } +auto LayoutState::popup_pane(Tab& tab, PopupLayout const& popup_layout, CreatePaneArgs args, + RenderThread& render_thread) -> di::Result<> { + if (hide_status_bar()) { + return tab.popup_pane(m_next_pane_id++, popup_layout, m_size, di::move(args), render_thread); + } + return tab.popup_pane(m_next_pane_id++, popup_layout, m_size.rows_shrinked(1), di::move(args), render_thread); +} + auto LayoutState::add_tab(CreatePaneArgs args, RenderThread& render_thread) -> di::Result<> { auto name = args.replay_path ? "capture"_s : di::back(di::PathView(args.command[0])).value_or(""_tsv) | di::transform([](char c) { diff --git a/src/layout_state.h b/src/layout_state.h index 530c5a1..1c0425b 100644 --- a/src/layout_state.h +++ b/src/layout_state.h @@ -3,6 +3,7 @@ #include "di/container/vector/vector.h" #include "tab.h" #include "ttx/layout.h" +#include "ttx/popup.h" namespace ttx { class LayoutState { @@ -15,6 +16,8 @@ class LayoutState { auto remove_pane(Tab& tab, Pane* pane) -> di::Box; auto add_pane(Tab& tab, CreatePaneArgs args, Direction direction, RenderThread& render_thread) -> di::Result<>; + auto popup_pane(Tab& tab, PopupLayout const& popup_layout, CreatePaneArgs args, RenderThread& render_thread) + -> di::Result<>; auto add_tab(CreatePaneArgs args, RenderThread& render_thread) -> di::Result<>; auto empty() const -> bool { return m_tabs.empty(); } @@ -31,6 +34,8 @@ class LayoutState { auto size() const -> Size { return m_size; } auto hide_status_bar() const -> bool { return m_hide_status_bar; } + auto active_popup() const -> di::Optional; + private: Size m_size; di::Vector> m_tabs {}; diff --git a/src/render.cpp b/src/render.cpp index 5a644d3..524d8ba 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -67,9 +67,9 @@ void RenderThread::render_thread() { m_layout_state.lock()->layout(ev.value()); } else if (auto ev = di::get_if(event)) { // Exit pane. - auto should_exit = m_layout_state.with_lock([&](LayoutState& state) { - state.remove_pane(*ev->tab, ev->pane); - return state.empty(); + auto [pane, should_exit] = m_layout_state.with_lock([&](LayoutState& state) { + auto pane = state.remove_pane(*ev->tab, ev->pane); + return di::Tuple { di::move(pane), state.empty() }; }); if (should_exit) { return; @@ -185,7 +185,17 @@ void RenderThread::do_render(Renderer& renderer) { auto cursor = di::Optional {}; - Render(renderer, cursor, tab, state, !state.hide_status_bar())(*tree); + // First render all panes in the layout tree. + auto render_fn = Render(renderer, cursor, tab, state, !state.hide_status_bar()); + render_fn(*tree); + + // If there is a popup, render it. + for (auto popup_layout : tab.popup_layout()) { + // For now, always invalidate the popup since we don't have proper damage tracking when + // panes overlap. + popup_layout.pane->invalidate_all(); + render_fn(popup_layout); + } (void) renderer.finish(dius::stdin, cursor.value_or({ .hidden = true })); }); diff --git a/src/tab.cpp b/src/tab.cpp index 3c8fabf..6a59a84 100644 --- a/src/tab.cpp +++ b/src/tab.cpp @@ -4,11 +4,16 @@ #include "dius/print.h" #include "render.h" #include "ttx/direction.h" +#include "ttx/popup.h" namespace ttx { void Tab::layout(Size const& size) { m_size = size; + if (m_popup) { + m_popup_layout = m_popup.value().layout(size); + } + if (m_full_screen_pane) { // In full screen mode, circumvent ordinary layout. m_full_screen_pane->resize(m_size); @@ -34,17 +39,27 @@ auto Tab::remove_pane(Pane* pane) -> di::Box { m_full_screen_pane = nullptr; } + if (pane) { + di::erase(m_panes_ordered_by_recency, pane); + } + // Clear active pane. if (m_active == pane) { - if (pane) { - di::erase(m_panes_ordered_by_recency, pane); - } auto candidates = m_panes_ordered_by_recency | di::transform([](Pane* pane) { return pane; }); set_active(candidates.front().value_or(nullptr)); } + // Clean the popup information if this pane was a popup. In this case, + // we don't return try to remove the pane from the layout tree. + if (m_popup && m_popup.value().pane.get() == pane) { + auto result = di::move(m_popup).value().pane; + m_popup_layout = {}; + m_popup = {}; + return result; + } + return m_layout_root.remove_pane(pane); } @@ -58,24 +73,7 @@ auto Tab::add_pane(u64 pane_id, Size const& size, CreatePaneArgs args, Direction return di::Unexpected(di::BasicError::InvalidArgument); } - auto maybe_pane = Pane::create( - pane_id, di::move(args), pane_layout->size, - [this, &render_thread](Pane& pane) { - render_thread.push_event(PaneExited(this, &pane)); - }, - [&render_thread](Pane&) { - render_thread.request_render(); - }, - [&render_thread](di::Span data) { - auto base64 = di::Base64View(data); - auto string = *di::present("\033]52;;{}\033\\"_sv, base64); - render_thread.push_event(WriteString(di::move(string))); - }, - [&render_thread](di::StringView apc_data) { - // Pass-through APC commands to host terminal. This makes kitty graphics "work". - auto string = *di::present("\033_{}\033\\"_sv, apc_data); - render_thread.push_event(WriteString(di::move(string))); - }); + auto maybe_pane = make_pane(pane_id, di::move(args), size, render_thread); if (!maybe_pane) { m_layout_root.remove_pane(nullptr); return di::Unexpected(di::move(maybe_pane).error()); @@ -89,6 +87,28 @@ auto Tab::add_pane(u64 pane_id, Size const& size, CreatePaneArgs args, Direction return {}; } +auto Tab::popup_pane(u64 pane_id, PopupLayout const& popup_layout, Size const& size, CreatePaneArgs args, + RenderThread& render_thread) -> di::Result<> { + m_popup = Popup { + .pane = nullptr, + .layout_config = popup_layout, + }; + m_popup_layout = m_popup.value().layout(size); + + auto maybe_pane = make_pane(pane_id, di::move(args), m_popup_layout.value().size, render_thread); + if (!maybe_pane) { + m_popup = {}; + m_popup_layout = {}; + return di::Unexpected(di::move(maybe_pane).error()); + } + m_popup.value().pane = di::move(maybe_pane).value(); + m_popup_layout.value().pane = m_popup.value().pane.get(); + + set_active(m_popup.value().pane.get()); + invalidate_all(); + return {}; +} + void Tab::navigate(NavigateDirection direction) { auto layout_entry = m_layout_tree->find_pane(m_active); if (!layout_entry) { @@ -202,4 +222,26 @@ auto Tab::set_is_active(bool b) -> bool { } return true; } + +auto Tab::make_pane(u64 pane_id, CreatePaneArgs args, Size const& size, RenderThread& render_thread) + -> di::Result> { + return Pane::create( + pane_id, di::move(args), size, + [this, &render_thread](Pane& pane) { + render_thread.push_event(PaneExited(this, &pane)); + }, + [&render_thread](Pane&) { + render_thread.request_render(); + }, + [&render_thread](di::Span data) { + auto base64 = di::Base64View(data); + auto string = *di::present("\033]52;;{}\033\\"_sv, base64); + render_thread.push_event(WriteString(di::move(string))); + }, + [&render_thread](di::StringView apc_data) { + // Pass-through APC commands to host terminal. This makes kitty graphics "work". + auto string = *di::present("\033_{}\033\\"_sv, apc_data); + render_thread.push_event(WriteString(di::move(string))); + }); +} } diff --git a/src/tab.h b/src/tab.h index bd03b1f..734bfb9 100644 --- a/src/tab.h +++ b/src/tab.h @@ -4,6 +4,7 @@ #include "di/reflect/prelude.h" #include "ttx/layout.h" #include "ttx/pane.h" +#include "ttx/popup.h" namespace ttx { class RenderThread; @@ -34,6 +35,8 @@ struct Tab { auto add_pane(u64 pane_id, Size const& size, CreatePaneArgs args, Direction direction, RenderThread& render_thread) -> di::Result<>; + auto popup_pane(u64 pane_id, PopupLayout const& popup_layout, Size const& size, CreatePaneArgs args, + RenderThread& render_thread) -> di::Result<>; void navigate(NavigateDirection direction); @@ -41,7 +44,7 @@ struct Tab { auto set_active(Pane* pane) -> bool; auto name() const -> di::StringView { return m_name; } - auto empty() const -> bool { return m_layout_root.empty(); } + auto empty() const -> bool { return m_layout_root.empty() && !m_popup; } auto layout_group() -> LayoutGroup& { return m_layout_root; } auto layout_tree() const -> di::Optional { @@ -71,7 +74,12 @@ struct Tab { } auto set_full_screen_pane(Pane* pane) -> bool; + auto popup_layout() const -> di::Optional { return m_popup_layout; } + private: + auto make_pane(u64 pane_id, CreatePaneArgs args, Size const& size, RenderThread& render_thread) + -> di::Result>; + Size m_size; di::String m_name; LayoutGroup m_layout_root {}; @@ -80,5 +88,7 @@ struct Tab { bool m_is_active { false }; Pane* m_active { nullptr }; Pane* m_full_screen_pane { nullptr }; + di::Optional m_popup; + di::Optional m_popup_layout; }; } From 2818918c00063ee8ee1269255ffc249cc7475635 Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sun, 13 Apr 2025 00:45:26 -0700 Subject: [PATCH 06/19] client: support spawning commands with pipe as stdin and stdout --- lib/include/ttx/pane.h | 50 +++++++-------- lib/src/pane.cpp | 137 +++++++++++++++++++++++++++++------------ src/tab.cpp | 7 ++- 3 files changed, 125 insertions(+), 69 deletions(-) diff --git a/lib/include/ttx/pane.h b/lib/include/ttx/pane.h index 502de80..b836fcc 100644 --- a/lib/include/ttx/pane.h +++ b/lib/include/ttx/pane.h @@ -24,34 +24,42 @@ struct CreatePaneArgs { di::Optional capture_command_output_path {}; di::Optional replay_path {}; di::Optional save_state_path {}; + di::Optional pipe_input {}; + bool pipe_output { false }; +}; + +class Pane; + +struct PaneHooks { + /// @brief Application controlled callback when the internal process exits. + di::Function did_exit; + + /// @brief controlled callback when the terminal buffer has updated. + di::Function did_update; + + /// @brief Application controlled callback when text is selected. + di::Function)> did_selection; + + /// @brief Application controlled callback when APC command is set. + di::Function apc_passthrough; }; class Pane { public: static auto create_from_replay(u64 id, di::PathView replay_path, di::Optional save_state_path, - Size const& size, di::Function did_exit, - di::Function did_update, - di::Function)> did_selection, - di::Function apc_passthrough) -> di::Result>; - static auto create(u64 id, CreatePaneArgs args, Size const& size, di::Function did_exit, - di::Function did_update, di::Function)> did_selection, - di::Function apc_passthrough) -> di::Result>; + Size const& size, PaneHooks hooks) -> di::Result>; + static auto create(u64 id, CreatePaneArgs args, Size const& size, PaneHooks hooks) -> di::Result>; // For testing, create a mock pane. This doesn't actually create a psuedo terminal or a subprocess. static auto create_mock() -> di::Box; explicit Pane(u64 id, dius::SyncFile pty_controller, Size const& size, dius::system::ProcessHandle process, - di::Function did_exit, di::Function did_update, - di::Function)> did_selection, - di::Function apc_passthrough) + PaneHooks hooks) : m_id(id) , m_pty_controller(di::move(pty_controller)) , m_terminal(di::in_place, id, m_pty_controller, size) , m_process(process) - , m_did_exit(di::move(did_exit)) - , m_did_update(di::move(did_update)) - , m_did_selection(di::move(did_selection)) - , m_apc_passthrough(di::move(apc_passthrough)) {} + , m_hooks(di::move(hooks)) {} ~Pane(); auto id() const { return m_id; } @@ -84,20 +92,12 @@ class Pane { u32 m_vertical_scroll_offset { 0 }; u32 m_horizontal_scroll_offset { 0 }; - // Application controlled callback when the internal process exits. - di::Function m_did_exit; - - // Application controlled callback when the terminal buffer has updated. - di::Function m_did_update; - - // Application controlled callback when text is selected. - di::Function)> m_did_selection; - - // Application controlled callback when APC command is set. - di::Function m_apc_passthrough; + PaneHooks m_hooks; // These are declared last, for when dius::Thread calls join() in the destructor. dius::Thread m_process_thread; dius::Thread m_reader_thread; + dius::Thread m_pipe_writer_thread; + dius::Thread m_pipe_reader_thread; }; } diff --git a/lib/src/pane.cpp b/lib/src/pane.cpp index 66c0637..ac219ae 100644 --- a/lib/src/pane.cpp +++ b/lib/src/pane.cpp @@ -18,7 +18,8 @@ #include "ttx/utf8_stream_decoder.h" namespace ttx { -static auto spawn_child(di::Vector command, dius::SyncFile& pty, Size const& size) +static auto spawn_child(di::Vector command, dius::SyncFile& pty, Size const& size, + i32 stdin_fd, i32 stdout_fd, di::Vector const& close_fds) -> di::Result { auto tty_path = TRY(pty.get_psuedo_terminal_path()); @@ -30,30 +31,31 @@ static auto spawn_child(di::Vector command, dius::Syn TRY(pty.set_tty_window_size(size.as_window_size())); #endif - return dius::system::Process(command | di::transform(di::to_owned) | di::to()) - .with_new_session() - .with_env("TERM"_ts, "xterm-256color"_ts) - .with_env("COLORTERM"_ts, "truecolor"_ts) - .with_file_open(0, di::move(tty_path), dius::OpenMode::ReadWrite) - .with_file_dup(0, 1) - .with_file_dup(0, 2) + auto result = dius::system::Process(command | di::transform(di::to_owned) | di::to()) + .with_new_session() + .with_env("TERM"_ts, "xterm-256color"_ts) + .with_env("COLORTERM"_ts, "truecolor"_ts) + .with_file_open(2, di::move(tty_path), dius::OpenMode::ReadWrite) + .with_file_dup(stdin_fd, 0) + .with_file_dup(stdout_fd, 1) #ifndef __linux__ - .with_tty_window_size(0, size.as_window_size()) - .with_controlling_tty(0) + .with_tty_window_size(2, size.as_window_size()) + .with_controlling_tty(2) #endif - .spawn(); + ; + for (auto close_fd : close_fds) { + result = di::move(result).with_file_close(close_fd); + } + + return di::move(result).spawn(); } auto Pane::create_from_replay(u64 id, di::PathView replay_path, di::Optional save_state_path, - Size const& size, di::Function did_exit, - di::Function did_update, - di::Function)> did_selection, - di::Function apc_passthrough) -> di::Result> { + Size const& size, PaneHooks hooks) -> di::Result> { auto replay_file = TRY(dius::open_sync(replay_path, dius::OpenMode::Readonly)); // When replaying content, there is no need for any threads, a psudeo terminal or a sub-process. - auto pane = di::make_box(id, dius::SyncFile(), size, dius::system::ProcessHandle(), di::move(did_exit), - di::move(did_update), di::move(did_selection), di::move(apc_passthrough)); + auto pane = di::make_box(id, dius::SyncFile(), size, dius::system::ProcessHandle(), di::move(hooks)); // Allow the terminal to use CSI 8; height; width; t. Normally this would be ignored since application // shouldn't control this. @@ -77,8 +79,8 @@ auto Pane::create_from_replay(u64 id, di::PathView replay_path, di::Optional(event)) { - if (pane->m_apc_passthrough) { - pane->m_apc_passthrough(ev->data); + if (pane->m_hooks.apc_passthrough) { + pane->m_hooks.apc_passthrough(ev->data); } return true; } @@ -92,8 +94,8 @@ auto Pane::create_from_replay(u64 id, di::PathView replay_path, di::Optionalm_did_selection) { - pane->m_did_selection(ev.data.span()); + if (pane->m_hooks.did_selection) { + pane->m_hooks.did_selection(ev.data.span()); } }), di::move(event)); @@ -112,12 +114,9 @@ auto Pane::create_from_replay(u64 id, di::PathView replay_path, di::Optional did_exit, - di::Function did_update, di::Function)> did_selection, - di::Function apc_passthrough) -> di::Result> { +auto Pane::create(u64 id, CreatePaneArgs args, Size const& size, PaneHooks hooks) -> di::Result> { if (args.replay_path) { - return create_from_replay(id, *args.replay_path, di::move(args.save_state_path), size, di::move(did_exit), - di::move(did_update), di::move(did_selection), di::move(apc_passthrough)); + return create_from_replay(id, *args.replay_path, di::move(args.save_state_path), size, di::move(hooks)); } auto capture_file = di::Optional {}; @@ -130,16 +129,35 @@ auto Pane::create(u64 id, CreatePaneArgs args, Size const& size, di::Function(id, di::move(pty_controller), size, process, di::move(did_exit), - di::move(did_update), di::move(did_selection), di::move(apc_passthrough)); + + // This logic allows piping input and output from the shell command. This ends up being very complicated... + auto stdin_fd = 2; + auto stdout_fd = 2; + auto close_fds = di::Vector {}; + auto write_pipes = di::Optional> {}; + auto read_pipes = di::Optional> {}; + if (args.pipe_input) { + write_pipes = TRY(dius::open_pipe(dius::OpenFlags::KeepAfterExec)); + stdin_fd = di::get<0>(write_pipes.value()).file_descriptor(); + close_fds.push_back(di::get<0>(write_pipes.value()).file_descriptor()); + close_fds.push_back(di::get<1>(write_pipes.value()).file_descriptor()); + } + if (args.pipe_output) { + read_pipes = TRY(dius::open_pipe(dius::OpenFlags::KeepAfterExec)); + stdout_fd = di::get<1>(read_pipes.value()).file_descriptor(); + close_fds.push_back(di::get<0>(read_pipes.value()).file_descriptor()); + close_fds.push_back(di::get<1>(read_pipes.value()).file_descriptor()); + } + + auto process = TRY(spawn_child(di::move(args.command), pty_controller, size, stdin_fd, stdout_fd, close_fds)); + auto pane = di::make_box(id, di::move(pty_controller), size, process, di::move(hooks)); pane->m_process_thread = TRY(dius::Thread::create([&pane = *pane] mutable { auto guard = di::ScopeExit([&] { pane.m_done.store(true, di::MemoryOrder::Release); - if (pane.m_did_exit) { - pane.m_did_exit(pane); + if (pane.m_hooks.did_exit) { + pane.m_hooks.did_exit(pane); } }); @@ -173,8 +191,8 @@ auto Pane::create(u64 id, CreatePaneArgs args, Size const& size, di::Function(event)) { - if (pane.m_apc_passthrough) { - pane.m_apc_passthrough(ev->data); + if (pane.m_hooks.apc_passthrough) { + pane.m_hooks.apc_passthrough(ev->data); } return true; } @@ -188,31 +206,68 @@ auto Pane::create(u64 id, CreatePaneArgs args, Size const& size, di::Functionm_pipe_writer_thread = TRY(dius::Thread::create( + [&pane = *pane, pipe = di::move(write_pipes).value(), input = di::move(args.pipe_input).value()] mutable { + auto& [read, write] = pipe; + (void) read.close(); + (void) write.write_exactly(di::as_bytes(input.span())); + (void) write.close(); + })); + } + if (args.pipe_output) { + pane->m_pipe_reader_thread = + TRY(dius::Thread::create([&pane = *pane, pipe = di::move(read_pipes).value()] mutable { + auto& [read, write] = pipe; + (void) write.close(); + + auto utf8_decoder = Utf8StreamDecoder {}; + + auto buffer = di::Vector {}; + buffer.resize(16384); + + auto contents = di::String {}; + while (!pane.m_done.load(di::MemoryOrder::Acquire)) { + auto nread = read.read_some(buffer.span()); + if (!nread.has_value()) { + break; + } + + auto utf8_string = utf8_decoder.decode(buffer | di::take(*nread)); + contents.append(utf8_string); + } + + (void) read.close(); + })); + } + return pane; } auto Pane::create_mock() -> di::Box { auto fake_psuedo_terminal = dius::SyncFile(); - return di::make_box(0, di::move(fake_psuedo_terminal), Size(1, 1), dius::system::ProcessHandle(), nullptr, - nullptr, nullptr, nullptr); + return di::make_box(0, di::move(fake_psuedo_terminal), Size(1, 1), dius::system::ProcessHandle(), + PaneHooks {}); } Pane::~Pane() { // TODO: timeout/skip waiting for processes to die after sending SIGHUP. (void) m_process.signal(dius::Signal::Hangup); + (void) m_pipe_reader_thread.join(); + (void) m_pipe_writer_thread.join(); (void) m_reader_thread.join(); (void) m_process_thread.join(); } @@ -371,8 +426,8 @@ auto Pane::event(MouseEvent const& event) -> bool { terminal.active_screen().screen.clear_selection(); return result; }); - if (!text.empty() && m_did_selection) { - m_did_selection(di::as_bytes(text.span())); + if (!text.empty() && m_hooks.did_selection) { + m_hooks.did_selection(di::as_bytes(text.span())); } return true; } diff --git a/src/tab.cpp b/src/tab.cpp index 6a59a84..1da6a64 100644 --- a/src/tab.cpp +++ b/src/tab.cpp @@ -225,8 +225,7 @@ auto Tab::set_is_active(bool b) -> bool { auto Tab::make_pane(u64 pane_id, CreatePaneArgs args, Size const& size, RenderThread& render_thread) -> di::Result> { - return Pane::create( - pane_id, di::move(args), size, + auto hooks = PaneHooks { [this, &render_thread](Pane& pane) { render_thread.push_event(PaneExited(this, &pane)); }, @@ -242,6 +241,8 @@ auto Tab::make_pane(u64 pane_id, CreatePaneArgs args, Size const& size, RenderTh // Pass-through APC commands to host terminal. This makes kitty graphics "work". auto string = *di::present("\033_{}\033\\"_sv, apc_data); render_thread.push_event(WriteString(di::move(string))); - }); + }, + }; + return Pane::create(pane_id, di::move(args), size, di::move(hooks)); } } From b4615b929706cf22c44b2c749ddbf9e6621b4058 Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sun, 13 Apr 2025 12:44:42 -0700 Subject: [PATCH 07/19] client: implement prefix+f to switch tabs with an fzf popup This is pretty cool, but because you can't rename tabs right now you can only tell the difference between tabs by the index. Next up is allowing user-provided names for tabs. --- lib/include/ttx/pane.h | 24 ++++++++++++---------- lib/src/pane.cpp | 21 +++++++++++--------- src/action.h | 2 +- src/actions.cpp | 42 +++++++++++++++++++++++++++++++++++++++ src/actions.h | 1 + src/fzf.cpp | 45 ++++++++++++++++++++++++++++++++++++++++++ src/fzf.h | 31 +++++++++++++++++++++++++++++ src/input.cpp | 4 ++-- src/input.h | 6 +++--- src/key_bind.cpp | 5 +++++ src/tab.cpp | 28 +++++++++++++++----------- src/ttx.cpp | 6 +++--- 12 files changed, 176 insertions(+), 39 deletions(-) create mode 100644 src/fzf.cpp create mode 100644 src/fzf.h diff --git a/lib/include/ttx/pane.h b/lib/include/ttx/pane.h index b836fcc..642ea05 100644 --- a/lib/include/ttx/pane.h +++ b/lib/include/ttx/pane.h @@ -19,15 +19,6 @@ #include "ttx/terminal.h" namespace ttx { -struct CreatePaneArgs { - di::Vector command {}; - di::Optional capture_command_output_path {}; - di::Optional replay_path {}; - di::Optional save_state_path {}; - di::Optional pipe_input {}; - bool pipe_output { false }; -}; - class Pane; struct PaneHooks { @@ -42,13 +33,26 @@ struct PaneHooks { /// @brief Application controlled callback when APC command is set. di::Function apc_passthrough; + + /// @brief Callback with the results on reading from the output pipe. + di::Function did_finish_output; +}; + +struct CreatePaneArgs { + di::Vector command {}; + di::Optional capture_command_output_path {}; + di::Optional replay_path {}; + di::Optional save_state_path {}; + di::Optional pipe_input {}; + bool pipe_output { false }; + PaneHooks hooks {}; }; class Pane { public: static auto create_from_replay(u64 id, di::PathView replay_path, di::Optional save_state_path, Size const& size, PaneHooks hooks) -> di::Result>; - static auto create(u64 id, CreatePaneArgs args, Size const& size, PaneHooks hooks) -> di::Result>; + static auto create(u64 id, CreatePaneArgs args, Size const& size) -> di::Result>; // For testing, create a mock pane. This doesn't actually create a psuedo terminal or a subprocess. static auto create_mock() -> di::Box; diff --git a/lib/src/pane.cpp b/lib/src/pane.cpp index ac219ae..afc1542 100644 --- a/lib/src/pane.cpp +++ b/lib/src/pane.cpp @@ -18,20 +18,19 @@ #include "ttx/utf8_stream_decoder.h" namespace ttx { -static auto spawn_child(di::Vector command, dius::SyncFile& pty, Size const& size, - i32 stdin_fd, i32 stdout_fd, di::Vector const& close_fds) - -> di::Result { +static auto spawn_child(di::Vector command, dius::SyncFile& pty, Size const& size, i32 stdin_fd, + i32 stdout_fd, di::Vector const& close_fds) -> di::Result { auto tty_path = TRY(pty.get_psuedo_terminal_path()); #ifdef __linux__ // On linux, we can set the terminal size in on the controlling pty. On MacOS, we need to do it in // the child. Doing so requires using fork() instead of posix_spawn() when spawning the process, so - // we try to avoid it. Additionally, opening the psudeo terminal implicitly will make it the controlling - // terminal, so there's no need to call ioctl(TIOCSCTTY). + // we try to avoid it. Additionally, opening the psudeo terminal on linux implicitly will make it + // the controlling terminal, so there's no need to call ioctl(TIOCSCTTY). TRY(pty.set_tty_window_size(size.as_window_size())); #endif - auto result = dius::system::Process(command | di::transform(di::to_owned) | di::to()) + auto result = dius::system::Process(di::move(command)) .with_new_session() .with_env("TERM"_ts, "xterm-256color"_ts) .with_env("COLORTERM"_ts, "truecolor"_ts) @@ -114,9 +113,9 @@ auto Pane::create_from_replay(u64 id, di::PathView replay_path, di::Optional di::Result> { +auto Pane::create(u64 id, CreatePaneArgs args, Size const& size) -> di::Result> { if (args.replay_path) { - return create_from_replay(id, *args.replay_path, di::move(args.save_state_path), size, di::move(hooks)); + return create_from_replay(id, *args.replay_path, di::move(args.save_state_path), size, di::move(args.hooks)); } auto capture_file = di::Optional {}; @@ -150,7 +149,7 @@ auto Pane::create(u64 id, CreatePaneArgs args, Size const& size, PaneHooks hooks } auto process = TRY(spawn_child(di::move(args.command), pty_controller, size, stdin_fd, stdout_fd, close_fds)); - auto pane = di::make_box(id, di::move(pty_controller), size, process, di::move(hooks)); + auto pane = di::make_box(id, di::move(pty_controller), size, process, di::move(args.hooks)); pane->m_process_thread = TRY(dius::Thread::create([&pane = *pane] mutable { auto guard = di::ScopeExit([&] { @@ -251,6 +250,10 @@ auto Pane::create(u64 id, CreatePaneArgs args, Size const& size, PaneHooks hooks } (void) read.close(); + + if (pane.m_hooks.did_finish_output) { + pane.m_hooks.did_finish_output(contents.view()); + } })); } diff --git a/src/action.h b/src/action.h index 4d7e8fb..e26937e 100644 --- a/src/action.h +++ b/src/action.h @@ -9,7 +9,7 @@ struct ActionContext { KeyEvent const& key_event; di::Synchronized& layout_state; RenderThread& render_thread; - di::Vector const& command; + di::Vector const& command; di::Atomic& done; }; diff --git a/src/actions.cpp b/src/actions.cpp index d55aecb..37af84b 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -2,6 +2,7 @@ #include "action.h" #include "di/format/prelude.h" +#include "fzf.h" #include "tab.h" #include "ttx/layout.h" @@ -98,6 +99,47 @@ auto switch_tab(usize index) -> Action { }; } +auto find_tab() -> Action { + return { + .description = "Find a tab in the current session by name using fzf"_s, + .apply = + [](ActionContext const& context) { + context.layout_state.with_lock([&](LayoutState& state) { + auto tab_names = di::Vector(); + for (auto [i, tab] : state.tabs() | di::enumerate) { + tab_names.push_back(*di::present("{} {}"_sv, i + 1, tab->name())); + } + + auto [create_pane_args, popup_layout] = Fzf() + .with_prompt("Switch to tab"_s) + .with_title("Tabs"_s) + .with_input(di::move(tab_names)) + .popup_args(); + create_pane_args.hooks.did_finish_output = [&layout_state = context.layout_state, + &render_thread = + context.render_thread](di::StringView contents) { + // Try to parse the first index. This will fail if contents is empty. + auto maybe_tab_index = di::parse_partial(contents); + if (!maybe_tab_index || maybe_tab_index == 0) { + return; + } + auto tab_index = maybe_tab_index.value() - 1; + layout_state.with_lock([&](LayoutState& state) { + if (auto tab = state.tabs().at(tab_index)) { + state.set_active_tab(tab.value().get()); + } + }); + render_thread.request_render(); + }; + for (auto& tab : state.active_tab()) { + (void) state.popup_pane(tab, popup_layout, di::move(create_pane_args), context.render_thread); + } + }); + context.render_thread.request_render(); + }, + }; +} + auto quit() -> Action { return { .description = "Quit ttx"_s, diff --git a/src/actions.h b/src/actions.h index 65854c8..ef6e9ef 100644 --- a/src/actions.h +++ b/src/actions.h @@ -11,6 +11,7 @@ auto navigate(NavigateDirection direction) -> Action; auto resize(ResizeDirection direction, i32 amount_in_cells) -> Action; auto create_tab() -> Action; auto switch_tab(usize index) -> Action; +auto find_tab() -> Action; auto quit() -> Action; auto save_state(di::Path path) -> Action; auto stop_capture() -> Action; diff --git a/src/fzf.cpp b/src/fzf.cpp new file mode 100644 index 0000000..faadd20 --- /dev/null +++ b/src/fzf.cpp @@ -0,0 +1,45 @@ +#include "fzf.h" + +#include "di/util/construct.h" +#include "ttx/pane.h" +#include "ttx/popup.h" + +namespace ttx { +auto Fzf::popup_args() && -> di::Tuple { + // For now, the default (centered) layout. + auto layout = PopupLayout {}; + + // Setup the pipes for fzf. + auto create_pane_args = CreatePaneArgs { + .pipe_input = m_input | di::join_with(U'\n') | di::to(), + .pipe_output = true, + }; + + // Build the command string based on the configuration. For now, most + // fields are hard-coded. + create_pane_args.command = di::Array { + "fzf"_ts, + "--border"_ts, + "--layout"_ts, + "reverse"_ts, + } | di::to(); + if (m_prompt) { + create_pane_args.command.push_back("--prompt"_ts); + + // Add '> ' as a prompt indicator + auto prompt_string = *di::present("{}> "_sv, m_prompt.value()); + create_pane_args.command.push_back(prompt_string.span() | di::transform(di::construct) | + di::to()); + } + if (m_title) { + create_pane_args.command.push_back("--border-label"_ts); + + // Add spacing for padding + auto label_string = *di::present(" {} "_sv, m_title.value()); + create_pane_args.command.push_back(label_string.span() | di::transform(di::construct) | + di::to()); + } + + return { di::move(create_pane_args), layout }; +} +} diff --git a/src/fzf.h b/src/fzf.h new file mode 100644 index 0000000..d343d5c --- /dev/null +++ b/src/fzf.h @@ -0,0 +1,31 @@ +#pragma once + +#include "ttx/pane.h" +#include "ttx/popup.h" + +namespace ttx { +class Fzf { +public: + auto with_prompt(di::String prompt) && -> Fzf { + m_prompt = di::move(prompt); + return di::move(*this); + } + + auto with_input(di::Vector input) && -> Fzf { + m_input = di::move(input); + return di::move(*this); + } + + auto with_title(di::String title) && -> Fzf { + m_title = di::move(title); + return di::move(*this); + } + + auto popup_args() && -> di::Tuple; + +private: + di::Optional m_prompt; + di::Optional m_title; + di::Vector m_input; +}; +} diff --git a/src/input.cpp b/src/input.cpp index 4966a02..8f5c572 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -15,7 +15,7 @@ #include "ttx/utf8_stream_decoder.h" namespace ttx { -auto InputThread::create(di::Vector command, di::Vector key_binds, +auto InputThread::create(di::Vector command, di::Vector key_binds, di::Synchronized& layout_state, RenderThread& render_thread) -> di::Result> { auto result = di::make_box(di::move(command), di::move(key_binds), layout_state, render_thread); @@ -25,7 +25,7 @@ auto InputThread::create(di::Vector command, di::Vect return result; } -InputThread::InputThread(di::Vector command, di::Vector key_binds, +InputThread::InputThread(di::Vector command, di::Vector key_binds, di::Synchronized& layout_state, RenderThread& render_thread) : m_key_binds(di::move(key_binds)) , m_command(di::move(command)) diff --git a/src/input.h b/src/input.h index 14482dc..c845e97 100644 --- a/src/input.h +++ b/src/input.h @@ -13,11 +13,11 @@ class RenderThread; class InputThread { public: - static auto create(di::Vector command, di::Vector key_binds, + static auto create(di::Vector command, di::Vector key_binds, di::Synchronized& layout_state, RenderThread& render_thread) -> di::Result>; - explicit InputThread(di::Vector command, di::Vector key_binds, + explicit InputThread(di::Vector command, di::Vector key_binds, di::Synchronized& layout_state, RenderThread& render_thread); ~InputThread(); @@ -35,7 +35,7 @@ class InputThread { InputMode m_mode { InputMode::Insert }; di::Vector m_key_binds; - di::Vector m_command; + di::Vector m_command; di::Atomic m_done { false }; di::Synchronized& m_layout_state; RenderThread& m_render_thread; diff --git a/src/key_bind.cpp b/src/key_bind.cpp index ec2141f..9c2530d 100644 --- a/src/key_bind.cpp +++ b/src/key_bind.cpp @@ -148,6 +148,11 @@ auto make_key_binds(Key prefix, di::Path save_state_path, bool replay_mode) -> d .mode = InputMode::Normal, .action = quit(), }); + result.push_back({ + .key = Key::F, + .mode = InputMode::Normal, + .action = find_tab(), + }); result.push_back({ .key = Key::R, .modifiers = Modifiers::Shift, diff --git a/src/tab.cpp b/src/tab.cpp index 1da6a64..f398d99 100644 --- a/src/tab.cpp +++ b/src/tab.cpp @@ -225,24 +225,30 @@ auto Tab::set_is_active(bool b) -> bool { auto Tab::make_pane(u64 pane_id, CreatePaneArgs args, Size const& size, RenderThread& render_thread) -> di::Result> { - auto hooks = PaneHooks { - [this, &render_thread](Pane& pane) { + if (!args.hooks.did_exit) { + args.hooks.did_exit = [this, &render_thread](Pane& pane) { render_thread.push_event(PaneExited(this, &pane)); - }, - [&render_thread](Pane&) { + }; + } + if (!args.hooks.did_update) { + args.hooks.did_update = [&render_thread](Pane&) { render_thread.request_render(); - }, - [&render_thread](di::Span data) { + }; + } + if (!args.hooks.did_selection) { + args.hooks.did_selection = [&render_thread](di::Span data) { auto base64 = di::Base64View(data); auto string = *di::present("\033]52;;{}\033\\"_sv, base64); render_thread.push_event(WriteString(di::move(string))); - }, - [&render_thread](di::StringView apc_data) { + }; + } + if (!args.hooks.apc_passthrough) { + args.hooks.apc_passthrough = [&render_thread](di::StringView apc_data) { // Pass-through APC commands to host terminal. This makes kitty graphics "work". auto string = *di::present("\033_{}\033\\"_sv, apc_data); render_thread.push_event(WriteString(di::move(string))); - }, - }; - return Pane::create(pane_id, di::move(args), size, di::move(hooks)); + }; + } + return Pane::create(pane_id, di::move(args), size); } } diff --git a/src/ttx.cpp b/src/ttx.cpp index a0b422b..cd2e4f4 100644 --- a/src/ttx.cpp +++ b/src/ttx.cpp @@ -69,6 +69,7 @@ static auto main(Args& args) -> di::Result { dius::eprintln("error: ttx requires at least 1 argument to know what file to replay"_sv); return di::Unexpected(di::BasicError::InvalidArgument); } + auto command = args.command | di::transform(di::to_owned) | di::to(); // Setup - log to file. [[maybe_unused]] auto& log = dius::stderr = TRY(dius::open_sync("/tmp/ttx.log"_pv, dius::OpenMode::WriteClobber)); @@ -141,8 +142,7 @@ static auto main(Args& args) -> di::Result { }); // Setup - input thread. - auto input_thread = - TRY(InputThread::create(di::clone(args.command), di::move(key_binds), layout_state, *render_thread)); + auto input_thread = TRY(InputThread::create(di::clone(command), di::move(key_binds), layout_state, *render_thread)); auto _ = di::ScopeExit([&] { input_thread->request_exit(); }); @@ -179,7 +179,7 @@ static auto main(Args& args) -> di::Result { } else { TRY(layout_state.get_assuming_no_concurrent_accesses().add_tab( { - .command = di::clone(args.command), + .command = di::clone(command), .capture_command_output_path = args.capture_command_output_path.transform(di::to_owned), }, *render_thread)); From 80f4958507cd6b5428532b6e672639587e70f912 Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sun, 13 Apr 2025 13:53:49 -0700 Subject: [PATCH 08/19] client: implement renaming a tab via fzf popup To get the desired behavior with fzf, its necessary to disable some of the UI components and pass nothing into the input pipe, and additionally use the --print-query option. The next commit will additionally force the prompt window to have a height of 3 and be top aligned, which works perfectly. Using fzf lets us avoid writing our own line editor just for certain text prompts. We should be able to use fzf for all the UI components in ttx. --- src/actions.cpp | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/actions.h | 1 + src/fzf.cpp | 14 ++++++++++---- src/fzf.h | 18 ++++++++++++++++++ src/key_bind.cpp | 5 +++++ src/tab.h | 2 ++ 6 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/actions.cpp b/src/actions.cpp index 37af84b..ca76e8e 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -83,6 +83,50 @@ auto create_tab() -> Action { }; } +auto rename_tab() -> Action { + return { + .description = "Rename the current active tab"_s, + .apply = + [](ActionContext const& context) { + context.layout_state.with_lock([&](LayoutState& state) { + if (!state.active_tab()) { + return; + } + auto& tab = state.active_tab().value(); + + auto [create_pane_args, popup_layout] = Fzf() + .with_title("Rename Tab"_s) + .with_prompt("Name"_s) + .with_no_separator() + .with_no_info() + .with_print_query() + .popup_args(); + create_pane_args.hooks.did_finish_output = di::make_function( + [&layout_state = context.layout_state, &tab, + &render_thread = context.render_thread](di::StringView contents) { + if (contents.empty()) { + return; + } + while (contents.ends_with(U'\n')) { + contents = contents.substr(contents.begin(), --contents.end()); + } + layout_state.with_lock([&](LayoutState&) { + // NOTE: we take the layout state lock to prevent data races. Also, the tab is + // guaranteed to still be alive because tabs won't be killed until all their panes have + // exited. And we put the popup in the tab. + tab.set_name(contents.to_owned()); + }); + render_thread.request_render(); + }); + for (auto& tab : state.active_tab()) { + (void) state.popup_pane(tab, popup_layout, di::move(create_pane_args), context.render_thread); + } + }); + context.render_thread.request_render(); + }, + }; +} + auto switch_tab(usize index) -> Action { ASSERT_GT(index, 0); return { diff --git a/src/actions.h b/src/actions.h index ef6e9ef..ae82d4b 100644 --- a/src/actions.h +++ b/src/actions.h @@ -10,6 +10,7 @@ auto reset_mode() -> Action; auto navigate(NavigateDirection direction) -> Action; auto resize(ResizeDirection direction, i32 amount_in_cells) -> Action; auto create_tab() -> Action; +auto rename_tab() -> Action; auto switch_tab(usize index) -> Action; auto find_tab() -> Action; auto quit() -> Action; diff --git a/src/fzf.cpp b/src/fzf.cpp index faadd20..3e4c2f0 100644 --- a/src/fzf.cpp +++ b/src/fzf.cpp @@ -18,10 +18,7 @@ auto Fzf::popup_args() && -> di::Tuple { // Build the command string based on the configuration. For now, most // fields are hard-coded. create_pane_args.command = di::Array { - "fzf"_ts, - "--border"_ts, - "--layout"_ts, - "reverse"_ts, + "fzf"_ts, "--border"_ts, "--layout"_ts, "reverse"_ts, "--info"_ts, "inline-right"_ts, } | di::to(); if (m_prompt) { create_pane_args.command.push_back("--prompt"_ts); @@ -39,6 +36,15 @@ auto Fzf::popup_args() && -> di::Tuple { create_pane_args.command.push_back(label_string.span() | di::transform(di::construct) | di::to()); } + if (m_no_info) { + create_pane_args.command.push_back("--no-info"_ts); + } + if (m_no_separator) { + create_pane_args.command.push_back("--no-separator"_ts); + } + if (m_print_query) { + create_pane_args.command.push_back("--print-query"_ts); + } return { di::move(create_pane_args), layout }; } diff --git a/src/fzf.h b/src/fzf.h index d343d5c..1e13c09 100644 --- a/src/fzf.h +++ b/src/fzf.h @@ -21,11 +21,29 @@ class Fzf { return di::move(*this); } + auto with_no_info(bool no_info = true) && -> Fzf { + m_no_info = no_info; + return di::move(*this); + } + + auto with_no_separator(bool no_separator = true) && -> Fzf { + m_no_separator = no_separator; + return di::move(*this); + } + + auto with_print_query(bool print_query = true) && -> Fzf { + m_print_query = print_query; + return di::move(*this); + } + auto popup_args() && -> di::Tuple; private: di::Optional m_prompt; di::Optional m_title; di::Vector m_input; + bool m_no_info { false }; + bool m_no_separator { false }; + bool m_print_query { false }; }; } diff --git a/src/key_bind.cpp b/src/key_bind.cpp index 9c2530d..45d9f46 100644 --- a/src/key_bind.cpp +++ b/src/key_bind.cpp @@ -153,6 +153,11 @@ auto make_key_binds(Key prefix, di::Path save_state_path, bool replay_mode) -> d .mode = InputMode::Normal, .action = find_tab(), }); + result.push_back({ + .key = Key::Comma, + .mode = InputMode::Normal, + .action = rename_tab(), + }); result.push_back({ .key = Key::R, .modifiers = Modifiers::Shift, diff --git a/src/tab.h b/src/tab.h index 734bfb9..f25560a 100644 --- a/src/tab.h +++ b/src/tab.h @@ -46,6 +46,8 @@ struct Tab { auto name() const -> di::StringView { return m_name; } auto empty() const -> bool { return m_layout_root.empty() && !m_popup; } + void set_name(di::String name) { m_name = di::move(name); } + auto layout_group() -> LayoutGroup& { return m_layout_root; } auto layout_tree() const -> di::Optional { if (!m_layout_tree) { From 610aa4498e36a68b252b7777797242d91c211dc7 Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sun, 13 Apr 2025 16:37:26 -0700 Subject: [PATCH 09/19] client: support fixed size popup windows Now the rename tab prompt is forced to have height 3, preventing fzf from showing any padding. --- lib/include/ttx/popup.h | 18 ++++++++++++++++-- lib/src/popup.cpp | 22 ++++++++++++++++------ lib/test/src/test_popup.cpp | 24 +++++++++++++++++++++--- src/actions.cpp | 5 ++--- src/fzf.cpp | 10 ++++++---- src/fzf.h | 32 ++++++++++++++++++++++++++++++++ src/render.cpp | 10 ++++++---- 7 files changed, 99 insertions(+), 22 deletions(-) diff --git a/lib/include/ttx/popup.h b/lib/include/ttx/popup.h index 4be5ad8..21745e6 100644 --- a/lib/include/ttx/popup.h +++ b/lib/include/ttx/popup.h @@ -12,10 +12,24 @@ enum class PopupAlignment { Center, }; +namespace detail { + struct RelatizeSizeTag { + using Type = i64; + }; + struct AbsoluteSizeTag { + using Type = u32; + }; +} + +using RelatizeSize = di::StrongInt; +using AbsoluteSize = di::StrongInt; + +using PopupSize = di::Variant; + struct PopupLayout { PopupAlignment alignment { PopupAlignment::Center }; - i64 relative_width { max_layout_precision / 2 }; // 50% width default - i64 relative_height { max_layout_precision / 2 }; // 50% height default + PopupSize width { RelatizeSize(max_layout_precision / 2) }; // 50% width default + PopupSize height { RelatizeSize(max_layout_precision / 2) }; // 50% height default }; struct Popup { diff --git a/lib/src/popup.cpp b/lib/src/popup.cpp index 37364cc..9f61382 100644 --- a/lib/src/popup.cpp +++ b/lib/src/popup.cpp @@ -4,14 +4,24 @@ #include "ttx/size.h" namespace ttx { +struct ResolveSize { + static auto operator()(Size const& total_size, RelatizeSize dimension, bool width) -> u32 { + auto size_dimension = width ? total_size.cols : total_size.rows; + return u32((di::Rational(dimension.raw_value(), max_layout_precision) * size_dimension).round()); + } + + static auto operator()(Size const&, AbsoluteSize dimension, bool) -> u32 { return dimension.raw_value(); } +}; + auto Popup::layout(Size const& size) -> LayoutEntry { - auto cols = (di::Rational(layout_config.relative_width, max_layout_precision) * size.cols).round(); - auto rows = (di::Rational(layout_config.relative_height, max_layout_precision) * size.rows).round(); - cols = di::max(cols, 1_i64); - rows = di::max(rows, 1_i64); + auto resolve_size = di::bind_front(ResolveSize {}, size); + auto cols = di::visit(di::bind_back(resolve_size, true), layout_config.width); + auto rows = di::visit(di::bind_back(resolve_size, false), layout_config.height); + cols = di::clamp(cols, 1_u32, size.cols); + rows = di::clamp(rows, 1_u32, size.rows); - auto empty_rows = u32(di::max(size.rows - rows, 0_i64)); - auto empty_cols = u32(di::max(size.cols - cols, 0_i64)); + auto empty_rows = di::max(size.rows - rows, 0_u32); + auto empty_cols = di::max(size.cols - cols, 0_u32); auto layout_size = Size(rows, cols, size.xpixels / cols, size.ypixels / rows); auto [r, c] = [&] -> di::Tuple { diff --git a/lib/test/src/test_popup.cpp b/lib/test/src/test_popup.cpp index 8b1d098..7ada948 100644 --- a/lib/test/src/test_popup.cpp +++ b/lib/test/src/test_popup.cpp @@ -17,8 +17,16 @@ static auto alignments() { auto make_input = [&](PopupAlignment alignment, i64 height_percent, i64 width_percent) { return PopupLayout { .alignment = alignment, - .relative_width = max_layout_precision / 100 * width_percent, - .relative_height = max_layout_precision / 100 * height_percent, + .width = RelatizeSize(max_layout_precision / 100 * width_percent), + .height = RelatizeSize(max_layout_precision / 100 * height_percent), + }; + }; + + auto make_fixed_input = [&](PopupAlignment alignment, u32 height, u32 width) { + return PopupLayout { + .alignment = alignment, + .width = AbsoluteSize(width), + .height = AbsoluteSize(height), }; }; @@ -31,7 +39,7 @@ static auto alignments() { }; auto cases = di::Array { - // Regular + // Fixed Case { .input = make_input(PopupAlignment::Center, 50, 50), .expected = make_entry(13, 15, 25, 30), @@ -57,6 +65,16 @@ static auto alignments() { .input = make_input(PopupAlignment::Center, 1, 1), .expected = make_entry(25, 30, 1, 1), }, + // Absolute + Case { + .input = make_fixed_input(PopupAlignment::Center, 25, 30), + .expected = make_entry(13, 15, 25, 30), + }, + // Too big + Case { + .input = make_fixed_input(PopupAlignment::Center, 100, 100), + .expected = make_entry(0, 0, 50, 60), + }, }; for (auto const& [input, expected] : cases) { diff --git a/src/actions.cpp b/src/actions.cpp index ca76e8e..5dea934 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -95,11 +95,10 @@ auto rename_tab() -> Action { auto& tab = state.active_tab().value(); auto [create_pane_args, popup_layout] = Fzf() + .as_text_box() .with_title("Rename Tab"_s) .with_prompt("Name"_s) - .with_no_separator() - .with_no_info() - .with_print_query() + .with_query(tab.name().to_owned()) .popup_args(); create_pane_args.hooks.did_finish_output = di::make_function( [&layout_state = context.layout_state, &tab, diff --git a/src/fzf.cpp b/src/fzf.cpp index 3e4c2f0..34744bf 100644 --- a/src/fzf.cpp +++ b/src/fzf.cpp @@ -6,9 +6,6 @@ namespace ttx { auto Fzf::popup_args() && -> di::Tuple { - // For now, the default (centered) layout. - auto layout = PopupLayout {}; - // Setup the pipes for fzf. auto create_pane_args = CreatePaneArgs { .pipe_input = m_input | di::join_with(U'\n') | di::to(), @@ -36,6 +33,11 @@ auto Fzf::popup_args() && -> di::Tuple { create_pane_args.command.push_back(label_string.span() | di::transform(di::construct) | di::to()); } + if (m_query) { + create_pane_args.command.push_back("--query"_ts); + create_pane_args.command.push_back(m_query.value().span() | di::transform(di::construct) | + di::to()); + } if (m_no_info) { create_pane_args.command.push_back("--no-info"_ts); } @@ -46,6 +48,6 @@ auto Fzf::popup_args() && -> di::Tuple { create_pane_args.command.push_back("--print-query"_ts); } - return { di::move(create_pane_args), layout }; + return { di::move(create_pane_args), m_layout }; } } diff --git a/src/fzf.h b/src/fzf.h index 1e13c09..a3bc2f4 100644 --- a/src/fzf.h +++ b/src/fzf.h @@ -21,6 +21,11 @@ class Fzf { return di::move(*this); } + auto with_query(di::String query) && -> Fzf { + m_query = di::move(query); + return di::move(*this); + } + auto with_no_info(bool no_info = true) && -> Fzf { m_no_info = no_info; return di::move(*this); @@ -36,12 +41,39 @@ class Fzf { return di::move(*this); } + auto with_alignment(PopupAlignment alignment) && -> Fzf { + m_layout.alignment = alignment; + return di::move(*this); + } + + auto with_width(PopupSize width) && -> Fzf { + m_layout.width = width; + return di::move(*this); + } + + auto with_height(PopupSize height) && -> Fzf { + m_layout.height = height; + return di::move(*this); + } + + /// @brief Convience method which configures fzf like a text box + auto as_text_box() && -> Fzf { + return di::move(*this) + .with_alignment(PopupAlignment::Top) + .with_height(AbsoluteSize(3)) + .with_no_info() + .with_no_separator() + .with_print_query(); + } + auto popup_args() && -> di::Tuple; private: di::Optional m_prompt; di::Optional m_title; + di::Optional m_query; di::Vector m_input; + PopupLayout m_layout; bool m_no_info { false }; bool m_no_separator { false }; bool m_print_query { false }; diff --git a/src/render.cpp b/src/render.cpp index 524d8ba..d621807 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -167,10 +167,12 @@ void RenderThread::do_render(Renderer& renderer) { if (!state.hide_status_bar()) { auto text = di::enumerate(state.tabs()) | di::transform(di::uncurry([&](usize i, di::Box const& tab) { auto sign = U' '; - if (tab->full_screen_pane()) { - sign = U'+'; - } else if (tab.get() == active_tab.data()) { - sign = U'*'; + if (tab.get() == active_tab.data()) { + if (tab->full_screen_pane()) { + sign = U'+'; + } else { + sign = U'*'; + } } return *di::present("[{}{} {}]"_sv, sign, i + 1, tab->name()); })) | From e93d393f4f5254c9c9f96358b95a00ff15ed66ad Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sun, 13 Apr 2025 21:28:22 -0700 Subject: [PATCH 10/19] client: implement key binds for next tab + prev tab --- src/actions.cpp | 53 +++++++++++++++++++++++++++++++++++++++++++++--- src/actions.h | 2 ++ src/key_bind.cpp | 10 +++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/actions.cpp b/src/actions.cpp index 5dea934..dfd259f 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -103,12 +103,12 @@ auto rename_tab() -> Action { create_pane_args.hooks.did_finish_output = di::make_function( [&layout_state = context.layout_state, &tab, &render_thread = context.render_thread](di::StringView contents) { - if (contents.empty()) { - return; - } while (contents.ends_with(U'\n')) { contents = contents.substr(contents.begin(), --contents.end()); } + if (contents.empty()) { + return; + } layout_state.with_lock([&](LayoutState&) { // NOTE: we take the layout state lock to prevent data races. Also, the tab is // guaranteed to still be alive because tabs won't be killed until all their panes have @@ -142,6 +142,53 @@ auto switch_tab(usize index) -> Action { }; } +auto switch_next_tab() -> Action { + return { + .description = "Switch to the next tab by numeric index"_s, + .apply = + [](ActionContext const& context) { + context.layout_state.with_lock([&](LayoutState& state) { + for (auto& tab : state.active_tab()) { + auto tabs = state.tabs() | di::transform(&di::Box::get); + auto it = di::find(tabs, &tab); + if (it == tabs.end()) { + return; + } + auto index = usize(it - tabs.begin()); + index++; + index %= tabs.size(); + state.set_active_tab(tabs[isize(index)]); + } + }); + context.render_thread.request_render(); + }, + }; +} + +auto switch_prev_tab() -> Action { + return { + .description = "Switch to the previous tab by numeric index"_s, + .apply = + [](ActionContext const& context) { + context.layout_state.with_lock([&](LayoutState& state) { + for (auto& tab : state.active_tab()) { + auto tabs = state.tabs() | di::transform(&di::Box::get); + auto it = di::find(tabs, &tab); + if (it == tabs.end()) { + return; + } + auto index = usize(it - tabs.begin()); + index += tabs.size(); + index--; + index %= tabs.size(); + state.set_active_tab(tabs[isize(index)]); + } + }); + context.render_thread.request_render(); + }, + }; +} + auto find_tab() -> Action { return { .description = "Find a tab in the current session by name using fzf"_s, diff --git a/src/actions.h b/src/actions.h index ae82d4b..1d42b3e 100644 --- a/src/actions.h +++ b/src/actions.h @@ -12,6 +12,8 @@ auto resize(ResizeDirection direction, i32 amount_in_cells) -> Action; auto create_tab() -> Action; auto rename_tab() -> Action; auto switch_tab(usize index) -> Action; +auto switch_next_tab() -> Action; +auto switch_prev_tab() -> Action; auto find_tab() -> Action; auto quit() -> Action; auto save_state(di::Path path) -> Action; diff --git a/src/key_bind.cpp b/src/key_bind.cpp index 45d9f46..83d141c 100644 --- a/src/key_bind.cpp +++ b/src/key_bind.cpp @@ -15,6 +15,16 @@ static auto make_switch_tab_binds(di::Vector& result) { .action = switch_tab(i + 1), }); } + result.push_back({ + .key = Key::P, + .mode = InputMode::Normal, + .action = switch_prev_tab(), + }); + result.push_back({ + .key = Key::N, + .mode = InputMode::Normal, + .action = switch_next_tab(), + }); } static auto make_navigate_binds(di::Vector& result, InputMode mode, InputMode next_mode) { From 6b43f38340a0ca83a4c68ca017079c1ca813dcf5 Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sun, 13 Apr 2025 22:36:07 -0700 Subject: [PATCH 11/19] client: implement support for a single session Next we can support creating, renaming, and switching between sessions. The code between tabs and sessions is fairly repetitive, but we can at least stop adding layers of indirection now. LayoutState -> Session -> Tab -> Pane --- src/actions.cpp | 173 +++++++++++++++++++++++-------------------- src/layout_state.cpp | 142 +++++++++++++++++++---------------- src/layout_state.h | 35 +++++---- src/render.cpp | 46 +++++++----- src/render.h | 1 + src/session.cpp | 135 +++++++++++++++++++++++++++++++++ src/session.h | 45 +++++++++++ src/tab.cpp | 2 +- src/tab.h | 7 +- src/ttx.cpp | 35 +++++---- 10 files changed, 425 insertions(+), 196 deletions(-) create mode 100644 src/session.cpp create mode 100644 src/session.h diff --git a/src/actions.cpp b/src/actions.cpp index dfd259f..02a0714 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -76,7 +76,9 @@ auto create_tab() -> Action { .apply = [](ActionContext const& context) { context.layout_state.with_lock([&](LayoutState& state) { - (void) state.add_tab({ .command = di::clone(context.command) }, context.render_thread); + for (auto& session : state.active_session()) { + (void) state.add_tab(session, { .command = di::clone(context.command) }, context.render_thread); + } }); context.render_thread.request_render(); }, @@ -89,36 +91,39 @@ auto rename_tab() -> Action { .apply = [](ActionContext const& context) { context.layout_state.with_lock([&](LayoutState& state) { - if (!state.active_tab()) { - return; - } - auto& tab = state.active_tab().value(); + for (auto& session : state.active_session()) { + if (!session.active_tab()) { + return; + } + auto& tab = session.active_tab().value(); - auto [create_pane_args, popup_layout] = Fzf() - .as_text_box() - .with_title("Rename Tab"_s) - .with_prompt("Name"_s) - .with_query(tab.name().to_owned()) - .popup_args(); - create_pane_args.hooks.did_finish_output = di::make_function( - [&layout_state = context.layout_state, &tab, - &render_thread = context.render_thread](di::StringView contents) { - while (contents.ends_with(U'\n')) { - contents = contents.substr(contents.begin(), --contents.end()); - } - if (contents.empty()) { - return; - } - layout_state.with_lock([&](LayoutState&) { - // NOTE: we take the layout state lock to prevent data races. Also, the tab is - // guaranteed to still be alive because tabs won't be killed until all their panes have - // exited. And we put the popup in the tab. - tab.set_name(contents.to_owned()); + auto [create_pane_args, popup_layout] = Fzf() + .as_text_box() + .with_title("Rename Tab"_s) + .with_prompt("Name"_s) + .with_query(tab.name().to_owned()) + .popup_args(); + create_pane_args.hooks.did_finish_output = di::make_function( + [&layout_state = context.layout_state, &tab, + &render_thread = context.render_thread](di::StringView contents) { + while (contents.ends_with(U'\n')) { + contents = contents.substr(contents.begin(), --contents.end()); + } + if (contents.empty()) { + return; + } + layout_state.with_lock([&](LayoutState&) { + // NOTE: we take the layout state lock to prevent data races. Also, the tab is + // guaranteed to still be alive because tabs won't be killed until all their panes + // have exited. And we put the popup in the tab. + tab.set_name(contents.to_owned()); + }); + render_thread.request_render(); }); - render_thread.request_render(); - }); - for (auto& tab : state.active_tab()) { - (void) state.popup_pane(tab, popup_layout, di::move(create_pane_args), context.render_thread); + for (auto& tab : state.active_tab()) { + (void) state.popup_pane(session, tab, popup_layout, di::move(create_pane_args), + context.render_thread); + } } }); context.render_thread.request_render(); @@ -133,8 +138,10 @@ auto switch_tab(usize index) -> Action { .apply = [index](ActionContext const& context) { context.layout_state.with_lock([&](LayoutState& state) { - if (auto tab = state.tabs().at(index - 1)) { - state.set_active_tab(tab.value().get()); + for (auto& session : state.active_session()) { + if (auto tab = session.tabs().at(index - 1)) { + state.set_active_tab(session, tab.value().get()); + } } }); context.render_thread.request_render(); @@ -148,16 +155,18 @@ auto switch_next_tab() -> Action { .apply = [](ActionContext const& context) { context.layout_state.with_lock([&](LayoutState& state) { - for (auto& tab : state.active_tab()) { - auto tabs = state.tabs() | di::transform(&di::Box::get); - auto it = di::find(tabs, &tab); - if (it == tabs.end()) { - return; + for (auto& session : state.active_session()) { + for (auto& tab : session.active_tab()) { + auto tabs = session.tabs() | di::transform(&di::Box::get); + auto it = di::find(tabs, &tab); + if (it == tabs.end()) { + return; + } + auto index = usize(it - tabs.begin()); + index++; + index %= tabs.size(); + state.set_active_tab(session, tabs[isize(index)]); } - auto index = usize(it - tabs.begin()); - index++; - index %= tabs.size(); - state.set_active_tab(tabs[isize(index)]); } }); context.render_thread.request_render(); @@ -171,17 +180,19 @@ auto switch_prev_tab() -> Action { .apply = [](ActionContext const& context) { context.layout_state.with_lock([&](LayoutState& state) { - for (auto& tab : state.active_tab()) { - auto tabs = state.tabs() | di::transform(&di::Box::get); - auto it = di::find(tabs, &tab); - if (it == tabs.end()) { - return; + for (auto& session : state.active_session()) { + for (auto& tab : session.active_tab()) { + auto tabs = session.tabs() | di::transform(&di::Box::get); + auto it = di::find(tabs, &tab); + if (it == tabs.end()) { + return; + } + auto index = usize(it - tabs.begin()); + index += tabs.size(); + index--; + index %= tabs.size(); + state.set_active_tab(session, tabs[isize(index)]); } - auto index = usize(it - tabs.begin()); - index += tabs.size(); - index--; - index %= tabs.size(); - state.set_active_tab(tabs[isize(index)]); } }); context.render_thread.request_render(); @@ -196,33 +207,36 @@ auto find_tab() -> Action { [](ActionContext const& context) { context.layout_state.with_lock([&](LayoutState& state) { auto tab_names = di::Vector(); - for (auto [i, tab] : state.tabs() | di::enumerate) { - tab_names.push_back(*di::present("{} {}"_sv, i + 1, tab->name())); - } + for (auto& session : state.active_session()) { + for (auto [i, tab] : session.tabs() | di::enumerate) { + tab_names.push_back(*di::present("{} {}"_sv, i + 1, tab->name())); + } - auto [create_pane_args, popup_layout] = Fzf() - .with_prompt("Switch to tab"_s) - .with_title("Tabs"_s) - .with_input(di::move(tab_names)) - .popup_args(); - create_pane_args.hooks.did_finish_output = [&layout_state = context.layout_state, - &render_thread = - context.render_thread](di::StringView contents) { - // Try to parse the first index. This will fail if contents is empty. - auto maybe_tab_index = di::parse_partial(contents); - if (!maybe_tab_index || maybe_tab_index == 0) { - return; + auto [create_pane_args, popup_layout] = Fzf() + .with_prompt("Switch to tab"_s) + .with_title("Tabs"_s) + .with_input(di::move(tab_names)) + .popup_args(); + create_pane_args.hooks.did_finish_output = di::make_function( + [&layout_state = context.layout_state, &render_thread = context.render_thread, + &session](di::StringView contents) { + // Try to parse the first index. This will fail if contents is empty. + auto maybe_tab_index = di::parse_partial(contents); + if (!maybe_tab_index || maybe_tab_index == 0) { + return; + } + auto tab_index = maybe_tab_index.value() - 1; + layout_state.with_lock([&](LayoutState& state) { + if (auto tab = session.tabs().at(tab_index)) { + state.set_active_tab(session, tab.value().get()); + } + }); + render_thread.request_render(); + }); + for (auto& tab : state.active_tab()) { + (void) state.popup_pane(session, tab, popup_layout, di::move(create_pane_args), + context.render_thread); } - auto tab_index = maybe_tab_index.value() - 1; - layout_state.with_lock([&](LayoutState& state) { - if (auto tab = state.tabs().at(tab_index)) { - state.set_active_tab(tab.value().get()); - } - }); - render_thread.request_render(); - }; - for (auto& tab : state.active_tab()) { - (void) state.popup_pane(tab, popup_layout, di::move(create_pane_args), context.render_thread); } }); context.render_thread.request_render(); @@ -325,11 +339,12 @@ auto add_pane(Direction direction) -> Action { .apply = [direction](ActionContext const& context) { context.layout_state.with_lock([&](LayoutState& state) { - if (!state.active_tab()) { - return; + for (auto& session : state.active_session()) { + for (auto& tab : session.active_tab()) { + (void) state.add_pane(session, tab, { .command = di::clone(context.command) }, direction, + context.render_thread); + } } - (void) state.add_pane(*state.active_tab(), { .command = di::clone(context.command) }, direction, - context.render_thread); }); context.render_thread.request_render(); }, diff --git a/src/layout_state.cpp b/src/layout_state.cpp index a907137..383f2e5 100644 --- a/src/layout_state.cpp +++ b/src/layout_state.cpp @@ -1,5 +1,9 @@ #include "layout_state.h" +#include "render.h" +#include "session.h" +#include "ttx/pane.h" + namespace ttx { LayoutState::LayoutState(Size const& size, bool hide_status_bar) : m_size(size), m_hide_status_bar(hide_status_bar) {} @@ -10,103 +14,117 @@ void LayoutState::layout(di::Optional size) { m_size = size.value(); } - if (!m_active_tab) { + if (!m_active_session) { return; } if (hide_status_bar()) { // Now status bar when forcing a single pane. - m_active_tab->layout(m_size); + m_active_session->layout(m_size); } else { - m_active_tab->layout(m_size.rows_shrinked(1)); + m_active_session->layout(m_size.rows_shrinked(1)); } } -auto LayoutState::set_active_tab(Tab* tab) -> bool { - if (m_active_tab == tab) { +auto LayoutState::set_active_session(Session* session) -> bool { + if (m_active_session == session) { return false; } - // Update tab with the new active status, and force layout - // when switching ot a new tab. This is needed because size - // changes are only sent to the active tab, so the old layout - // could be stale. - if (m_active_tab) { - m_active_tab->set_is_active(false); + if (m_active_session) { + m_active_session->set_is_active(false); } - m_active_tab = tab; - if (m_active_tab) { - m_active_tab->set_is_active(true); + m_active_session = session; + if (m_active_session) { + m_active_session->set_is_active(true); + // Force a layout() when switching which session is rendered, + // since we resize things only when rendered. layout(); } return true; } -void LayoutState::remove_tab(Tab& tab) { - // For now, ASSERT() there are no panes in the tab. If there were, we'd +auto LayoutState::set_active_tab(Session& session, Tab* tab) -> bool { + set_active_session(&session); + return session.set_active_tab(tab); +} + +void LayoutState::remove_tab(Session& session, Tab& tab) { + session.remove_tab(tab); + if (session.empty()) { + remove_session(session); + } +} + +auto LayoutState::remove_pane(Session& session, Tab& tab, Pane* pane) -> di::Box { + auto result = session.remove_pane(tab, pane); + if (session.empty()) { + remove_session(session); + } + return result; +} + +void LayoutState::remove_session(Session& session) { + // For now, ASSERT() there are no panes in the session. If there were, we'd // need to make sure not to destroy the panes while we hold the lock. - ASSERT(tab.empty()); - - // Clear active tab. - if (m_active_tab == &tab) { - auto* it = di::find(m_tabs, &tab, &di::Box::get); - if (it == m_tabs.end()) { - set_active_tab(m_tabs.at(0).transform(&di::Box::get).value_or(nullptr)); - } else if (m_tabs.size() == 1) { - set_active_tab(nullptr); + ASSERT(session.empty()); + + // Clear active session. + if (m_active_session == &session) { + auto* it = di::find(m_sessions, &session, [](Session const& session) { + return &session; + }); + if (it == m_sessions.end()) { + set_active_session(m_sessions.at(0).data()); + } else if (m_sessions.size() == 1) { + set_active_session(nullptr); } else { - auto index = usize(it - m_tabs.begin()); - if (index == m_tabs.size() - 1) { - set_active_tab(m_tabs[index - 1].get()); + auto index = usize(it - m_sessions.begin()); + if (index == m_sessions.size() - 1) { + set_active_session(&m_sessions[index - 1]); } else { - set_active_tab(m_tabs[index + 1].get()); + set_active_session(&m_sessions[index + 1]); } } } - // Delete tab. - di::erase_if(m_tabs, [&](di::Box const& pointer) { - return pointer.get() == &tab; + // Delete session. + di::erase_if(m_sessions, [&](Session const& item) { + return &item == &session; }); } -auto LayoutState::remove_pane(Tab& tab, Pane* pane) -> di::Box { - auto result = tab.remove_pane(pane); - if (tab.empty()) { - remove_tab(tab); - } else if (result && &tab == m_active_tab) { - layout(); - } - return result; +auto LayoutState::add_pane(Session& session, Tab& tab, CreatePaneArgs args, Direction direction, + RenderThread& render_thread) -> di::Result<> { + set_active_session(&session); + return session.add_pane(tab, m_next_pane_id++, di::move(args), direction, render_thread); } -auto LayoutState::add_pane(Tab& tab, CreatePaneArgs args, Direction direction, RenderThread& render_thread) - -> di::Result<> { - if (hide_status_bar()) { - return tab.add_pane(m_next_pane_id++, m_size, di::move(args), direction, render_thread); - } - return tab.add_pane(m_next_pane_id++, m_size.rows_shrinked(1), di::move(args), direction, render_thread); +auto LayoutState::popup_pane(Session& session, Tab& tab, PopupLayout const& popup_layout, CreatePaneArgs args, + RenderThread& render_thread) -> di::Result<> { + set_active_session(&session); + return session.popup_pane(tab, m_next_pane_id++, popup_layout, di::move(args), render_thread); } -auto LayoutState::popup_pane(Tab& tab, PopupLayout const& popup_layout, CreatePaneArgs args, - RenderThread& render_thread) -> di::Result<> { - if (hide_status_bar()) { - return tab.popup_pane(m_next_pane_id++, popup_layout, m_size, di::move(args), render_thread); - } - return tab.popup_pane(m_next_pane_id++, popup_layout, m_size.rows_shrinked(1), di::move(args), render_thread); +auto LayoutState::add_tab(Session& session, CreatePaneArgs args, RenderThread& render_thread) -> di::Result<> { + set_active_session(&session); + return session.add_tab(di::move(args), m_next_pane_id++, render_thread); } -auto LayoutState::add_tab(CreatePaneArgs args, RenderThread& render_thread) -> di::Result<> { - auto name = args.replay_path ? "capture"_s - : di::back(di::PathView(args.command[0])).value_or(""_tsv) | di::transform([](char c) { - return c32(c); - }) | di::to(); - auto tab = di::make_box(di::move(name)); - TRY(add_pane(*tab, di::move(args), Direction::None, render_thread)); +auto LayoutState::add_session(CreatePaneArgs args, RenderThread& render_thread) -> di::Result<> { + auto name = di::to_string(m_next_session_id++); + auto& session = m_sessions.emplace_back(di::move(name)); + return add_tab(session, di::move(args), render_thread); +} - set_active_tab(tab.get()); - m_tabs.push_back(di::move(tab)); +auto LayoutState::active_session() const -> di::Optional { + if (!m_active_session) { + return {}; + } + return *m_active_session; +} - return {}; +auto LayoutState::active_tab() const -> di::Optional { + return active_session().and_then(&Session::active_tab); } auto LayoutState::active_pane() const -> di::Optional { diff --git a/src/layout_state.h b/src/layout_state.h index 1c0425b..e0c8ff7 100644 --- a/src/layout_state.h +++ b/src/layout_state.h @@ -1,6 +1,7 @@ #pragma once #include "di/container/vector/vector.h" +#include "session.h" #include "tab.h" #include "ttx/layout.h" #include "ttx/popup.h" @@ -11,23 +12,24 @@ class LayoutState { explicit LayoutState(Size const& size, bool hide_status_bar); void layout(di::Optional size = {}); - auto set_active_tab(Tab* tab) -> bool; - void remove_tab(Tab& tab); - auto remove_pane(Tab& tab, Pane* pane) -> di::Box; + auto set_active_tab(Session& session, Tab* tab) -> bool; + void remove_tab(Session& session, Tab& tab); + auto remove_pane(Session& session, Tab& tab, Pane* pane) -> di::Box; - auto add_pane(Tab& tab, CreatePaneArgs args, Direction direction, RenderThread& render_thread) -> di::Result<>; - auto popup_pane(Tab& tab, PopupLayout const& popup_layout, CreatePaneArgs args, RenderThread& render_thread) + auto add_pane(Session& session, Tab& tab, CreatePaneArgs args, Direction direction, RenderThread& render_thread) -> di::Result<>; - auto add_tab(CreatePaneArgs args, RenderThread& render_thread) -> di::Result<>; + auto popup_pane(Session& session, Tab& tab, PopupLayout const& popup_layout, CreatePaneArgs args, + RenderThread& render_thread) -> di::Result<>; + auto add_tab(Session& session, CreatePaneArgs args, RenderThread& render_thread) -> di::Result<>; - auto empty() const -> bool { return m_tabs.empty(); } - auto tabs() -> di::Vector>& { return m_tabs; } - auto active_tab() const -> di::Optional { - if (!m_active_tab) { - return {}; - } - return *m_active_tab; - } + auto sessions() -> di::Vector& { return m_sessions; } + auto add_session(CreatePaneArgs args, RenderThread& render_thread) -> di::Result<>; + void remove_session(Session& session); + auto set_active_session(Session* session) -> bool; + auto active_session() const -> di::Optional; + + auto empty() const -> bool { return m_sessions.empty(); } + auto active_tab() const -> di::Optional; auto active_pane() const -> di::Optional; auto full_screen_pane() const -> di::Optional; @@ -38,9 +40,10 @@ class LayoutState { private: Size m_size; - di::Vector> m_tabs {}; - Tab* m_active_tab { nullptr }; + di::Vector m_sessions; + Session* m_active_session { nullptr }; u64 m_next_pane_id { 1 }; + u64 m_next_session_id { 1 }; bool m_hide_status_bar { false }; }; } diff --git a/src/render.cpp b/src/render.cpp index d621807..2ea7fea 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -68,7 +68,7 @@ void RenderThread::render_thread() { } else if (auto ev = di::get_if(event)) { // Exit pane. auto [pane, should_exit] = m_layout_state.with_lock([&](LayoutState& state) { - auto pane = state.remove_pane(*ev->tab, ev->pane); + auto pane = state.remove_pane(*ev->session, *ev->tab, ev->pane); return di::Tuple { di::move(pane), state.empty() }; }); if (should_exit) { @@ -165,24 +165,34 @@ void RenderThread::do_render(Renderer& renderer) { // Status bar. if (!state.hide_status_bar()) { - auto text = di::enumerate(state.tabs()) | di::transform(di::uncurry([&](usize i, di::Box const& tab) { - auto sign = U' '; - if (tab.get() == active_tab.data()) { - if (tab->full_screen_pane()) { - sign = U'+'; - } else { - sign = U'*'; + for (auto& session : state.active_session()) { + auto text = di::enumerate(session.tabs()) | + di::transform(di::uncurry([&](usize i, di::Box const& tab) { + auto sign = U' '; + if (tab.get() == active_tab.data()) { + if (tab->full_screen_pane()) { + sign = U'+'; + } else { + sign = U'*'; + } } - } - return *di::present("[{}{} {}]"_sv, sign, i + 1, tab->name()); - })) | - di::join_with(U' ') | di::to(); - renderer.clear_row(0); - renderer.put_text(di::to_string(m_input_status.mode).view(), 0, 0, - GraphicsRendition { - .font_weight = FontWeight::Bold, - }); - renderer.put_text(text.view(), 0, 7); + return *di::present("[{}{} {}]"_sv, sign, i + 1, tab->name()); + })) | + di::join_with(U' ') | di::to(); + renderer.clear_row(0); + renderer.put_text(di::to_string(m_input_status.mode).view(), 0, 0, + GraphicsRendition { + .font_weight = FontWeight::Bold, + }); + renderer.put_text(text.view(), 0, 7); + + // TODO: horizontal scrolling on overflow + + // TODO: this code isn't correct if the session name contains any multi-code point grapheme clusters. + // TODO: handle case where session name is longer than the status bar width. + auto session_text = *di::present("[{}]"_sv, session.name()); + renderer.put_text(session_text.view(), 0, state.size().cols - di::distance(session_text)); + } } auto cursor = di::Optional {}; diff --git a/src/render.h b/src/render.h index 6e9c68e..9308146 100644 --- a/src/render.h +++ b/src/render.h @@ -10,6 +10,7 @@ namespace ttx { struct PaneExited { + Session* session = nullptr; Tab* tab = nullptr; Pane* pane = nullptr; }; diff --git a/src/session.cpp b/src/session.cpp new file mode 100644 index 0000000..d3c9bce --- /dev/null +++ b/src/session.cpp @@ -0,0 +1,135 @@ +#include "session.h" + +#include "ttx/pane.h" + +namespace ttx { +void Session::layout(di::Optional size) { + if (!size) { + size = m_size; + } else { + m_size = size.value(); + } + + if (!m_active_tab) { + return; + } + m_active_tab->layout(m_size); +} + +auto Session::set_active_tab(Tab* tab) -> bool { + if (m_active_tab == tab) { + return false; + } + + // Update tab with the new active status, only in cases + // where this session is active. + if (is_active() && m_active_tab) { + m_active_tab->set_is_active(false); + } + m_active_tab = tab; + if (is_active() && m_active_tab) { + m_active_tab->set_is_active(true); + layout(); + } + return true; +} + +void Session::remove_tab(Tab& tab) { + // For now, ASSERT() there are no panes in the tab. If there were, we'd + // need to make sure not to destroy the panes while we hold the lock. + ASSERT(tab.empty()); + + // Clear active tab. + if (m_active_tab == &tab) { + auto* it = di::find(m_tabs, &tab, &di::Box::get); + if (it == m_tabs.end()) { + set_active_tab(m_tabs.at(0).transform(&di::Box::get).value_or(nullptr)); + } else if (m_tabs.size() == 1) { + set_active_tab(nullptr); + } else { + auto index = usize(it - m_tabs.begin()); + if (index == m_tabs.size() - 1) { + set_active_tab(m_tabs[index - 1].get()); + } else { + set_active_tab(m_tabs[index + 1].get()); + } + } + } + + // Delete tab. + di::erase_if(m_tabs, [&](di::Box const& pointer) { + return pointer.get() == &tab; + }); +} + +auto Session::remove_pane(Tab& tab, Pane* pane) -> di::Box { + auto result = tab.remove_pane(pane); + if (tab.empty()) { + remove_tab(tab); + } else if (result && &tab == m_active_tab) { + layout(); + } + return result; +} + +auto Session::add_pane(Tab& tab, u64 pane_id, CreatePaneArgs args, Direction direction, RenderThread& render_thread) + -> di::Result<> { + return tab.add_pane(pane_id, m_size, di::move(args), direction, render_thread); +} + +auto Session::popup_pane(Tab& tab, u64 pane_id, PopupLayout const& popup_layout, CreatePaneArgs args, + RenderThread& render_thread) -> di::Result<> { + return tab.popup_pane(pane_id, popup_layout, m_size, di::move(args), render_thread); +} + +auto Session::add_tab(CreatePaneArgs args, u64 pane_id, RenderThread& render_thread) -> di::Result<> { + auto name = args.replay_path ? "capture"_s + : di::back(di::PathView(args.command[0])).value_or(""_tsv) | di::transform([](char c) { + return c32(c); + }) | di::to(); + auto tab = di::make_box(this, di::move(name)); + TRY(add_pane(*tab, pane_id, di::move(args), Direction::None, render_thread)); + + set_active_tab(tab.get()); + m_tabs.push_back(di::move(tab)); + + return {}; +} + +auto Session::active_tab() const -> di::Optional { + if (!m_active_tab) { + return {}; + } + return *m_active_tab; +} + +auto Session::active_pane() const -> di::Optional { + if (!active_tab()) { + return {}; + } + return active_tab()->active(); +} + +auto Session::full_screen_pane() const -> di::Optional { + if (!active_tab()) { + return {}; + } + return active_tab()->full_screen_pane(); +} + +auto Session::set_is_active(bool b) -> bool { + if (m_is_active == b) { + return false; + } + + // Send focus in/out events appropriately. + if (is_active() && m_active_tab) { + m_active_tab->set_is_active(false); + } + m_is_active = b; + if (is_active() && m_active_tab) { + m_active_tab->set_is_active(true); + } + return true; +} +} diff --git a/src/session.h b/src/session.h new file mode 100644 index 0000000..599fc3a --- /dev/null +++ b/src/session.h @@ -0,0 +1,45 @@ +#pragma once + +#include "di/container/vector/vector.h" +#include "tab.h" +#include "ttx/popup.h" + +namespace ttx { +/// @brief Represents a "session" (like in tmux) +class Session { +public: + explicit Session(di::String name) : m_name(di::move(name)) {} + + void layout(di::Optional size = {}); + auto set_active_tab(Tab* tab) -> bool; + void remove_tab(Tab& tab); + auto remove_pane(Tab& tab, Pane* pane) -> di::Box; + + void set_name(di::String name) { m_name = di::move(name); } + auto name() const -> di::StringView { return m_name; } + + auto add_pane(Tab& tab, u64 pane_id, CreatePaneArgs args, Direction direction, RenderThread& render_thread) + -> di::Result<>; + auto popup_pane(Tab& tab, u64 pane_id, PopupLayout const& popup_layout, CreatePaneArgs args, + RenderThread& render_thread) -> di::Result<>; + auto add_tab(CreatePaneArgs args, u64 pane_id, RenderThread& render_thread) -> di::Result<>; + + auto empty() const -> bool { return m_tabs.empty(); } + auto tabs() -> di::Vector>& { return m_tabs; } + auto active_tab() const -> di::Optional; + + auto active_pane() const -> di::Optional; + auto full_screen_pane() const -> di::Optional; + auto size() const -> Size { return m_size; } + + auto is_active() const -> bool { return m_is_active; } + auto set_is_active(bool b) -> bool; + +private: + di::String m_name; + Size m_size; + di::Vector> m_tabs {}; + Tab* m_active_tab { nullptr }; + bool m_is_active { false }; +}; +} diff --git a/src/tab.cpp b/src/tab.cpp index f398d99..8f23e37 100644 --- a/src/tab.cpp +++ b/src/tab.cpp @@ -227,7 +227,7 @@ auto Tab::make_pane(u64 pane_id, CreatePaneArgs args, Size const& size, RenderTh -> di::Result> { if (!args.hooks.did_exit) { args.hooks.did_exit = [this, &render_thread](Pane& pane) { - render_thread.push_event(PaneExited(this, &pane)); + render_thread.push_event(PaneExited(m_session, this, &pane)); }; } if (!args.hooks.did_update) { diff --git a/src/tab.h b/src/tab.h index f25560a..4f00bad 100644 --- a/src/tab.h +++ b/src/tab.h @@ -22,10 +22,12 @@ constexpr auto tag_invoke(di::Tag, di::InPlaceType, di::enumerator<"Down", Down>); } +class Session; + // Corresponds to tmux window. -struct Tab { +class Tab { public: - explicit Tab(di::String name) : m_name(di::move(name)) {} + explicit Tab(Session* session, di::String name) : m_session(session), m_name(di::move(name)) {} void layout(Size const& size); void invalidate_all(); @@ -82,6 +84,7 @@ struct Tab { auto make_pane(u64 pane_id, CreatePaneArgs args, Size const& size, RenderThread& render_thread) -> di::Result>; + Session* m_session { nullptr }; Size m_size; di::String m_name; LayoutGroup m_layout_root {}; diff --git a/src/ttx.cpp b/src/ttx.cpp index cd2e4f4..a7d81a5 100644 --- a/src/ttx.cpp +++ b/src/ttx.cpp @@ -1,24 +1,13 @@ #include "di/cli/parser.h" -#include "di/container/ring/ring.h" #include "di/container/string/string_view.h" #include "di/io/writer_print.h" -#include "di/serialization/base64.h" #include "di/sync/synchronized.h" -#include "di/util/construct.h" -#include "dius/condition_variable.h" #include "dius/main.h" #include "dius/sync_file.h" #include "dius/system/process.h" -#include "dius/thread.h" -#include "dius/tty.h" #include "input.h" #include "layout_state.h" #include "render.h" -#include "ttx/focus_event.h" -#include "ttx/layout.h" -#include "ttx/pane.h" -#include "ttx/terminal_input.h" -#include "ttx/utf8_stream_decoder.h" namespace ttx { struct Args { @@ -151,9 +140,18 @@ static auto main(Args& args) -> di::Result { auto _ = di::ScopeExit([&] { layout_state.with_lock([&](LayoutState& state) { while (!state.empty()) { - auto& tab = **state.tabs().front(); - for (auto* pane : tab.panes()) { - state.remove_pane(tab, pane); + auto& session = *state.sessions().front(); + while (!session.empty()) { + auto last_tab = session.tabs().size() == 1; + auto& tab = **session.tabs().front(); + for (auto* pane : tab.panes()) { + state.remove_pane(session, tab, pane); + } + // We must explicitly check this because the session object is destroyed + // after the last tab is removed. + if (last_tab) { + break; + } } } }); @@ -164,7 +162,7 @@ static auto main(Args& args) -> di::Result { auto& state = layout_state.get_assuming_no_concurrent_accesses(); for (auto replay_path : args.command) { if (state.empty()) { - TRY(state.add_tab( + TRY(state.add_session( { .replay_path = di::PathView(replay_path).to_owned(), .save_state_path = args.save_state_path.transform(di::to_owned), @@ -172,12 +170,13 @@ static auto main(Args& args) -> di::Result { *render_thread)); } else { // Horizontal split (means vertical layout) - TRY(state.add_pane(*state.active_tab(), { .replay_path = di::PathView(replay_path).to_owned() }, - Direction::Vertical, *render_thread)); + TRY(state.add_pane(*state.active_session(), *state.active_tab(), + { .replay_path = di::PathView(replay_path).to_owned() }, Direction::Vertical, + *render_thread)); } } } else { - TRY(layout_state.get_assuming_no_concurrent_accesses().add_tab( + TRY(layout_state.get_assuming_no_concurrent_accesses().add_session( { .command = di::clone(command), .capture_command_output_path = args.capture_command_output_path.transform(di::to_owned), From 65dc8b2971b959c19485d54eb79968d5dca41a96 Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sun, 13 Apr 2025 23:15:04 -0700 Subject: [PATCH 12/19] meta: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 97d0825..e5eaeef 100644 --- a/flake.lock +++ b/flake.lock @@ -6,11 +6,11 @@ "nixpkgs": ["nixpkgs"] }, "locked": { - "lastModified": 1744089186, - "narHash": "sha256-0/044okI7No2Go5iZjLAHq06iyAU95mIyv5+jWC3WBU=", + "lastModified": 1744611131, + "narHash": "sha256-LtHtOq1WBQM7i/6OlP+9yhteDwd2fHDuxlCFLwbl1kI=", "owner": "coletrammer", "repo": "di", - "rev": "a75de4c5c211f33cf864b3a62a45aab30ee1710a", + "rev": "451f3d9329274d831e78b2e63b5fa12d36b40c1a", "type": "github" }, "original": { @@ -26,11 +26,11 @@ "nixpkgs": ["nixpkgs"] }, "locked": { - "lastModified": 1742167628, - "narHash": "sha256-9sekISvEV3P+PJVaH9thzP7WvNC5aiCwyR8HqYiYS9w=", + "lastModified": 1744777067, + "narHash": "sha256-9Aa7o+jsMY5iO5rKcVMArnXSErt8GwnsWz97luwinKk=", "owner": "coletrammer", "repo": "dius", - "rev": "b0a5d50e67700bd3b6cb1819a7c242dfad45632f", + "rev": "b113535e8bc0e0707686b4563926f48826746628", "type": "github" }, "original": { From 81aa5be3dfbd9da6836adb3c56c7fc97819949ad Mon Sep 17 00:00:00 2001 From: coletrammer Date: Sun, 13 Apr 2025 23:35:35 -0700 Subject: [PATCH 13/19] client: implement pane switching, finding, and creating key binds --- src/actions.cpp | 149 +++++++++++++++++++++++++++++++++++++++++++++++ src/actions.h | 5 ++ src/fzf.cpp | 3 +- src/key_bind.cpp | 42 ++++++++++++- 4 files changed, 195 insertions(+), 4 deletions(-) diff --git a/src/actions.cpp b/src/actions.cpp index 02a0714..3344ba4 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -244,6 +244,155 @@ auto find_tab() -> Action { }; } +auto create_session() -> Action { + return { + .description = "Create a new session"_s, + .apply = + [](ActionContext const& context) { + context.layout_state.with_lock([&](LayoutState& state) { + (void) state.add_session({ .command = di::clone(context.command) }, context.render_thread); + }); + context.render_thread.request_render(); + }, + }; +} + +auto rename_session() -> Action { + return { + .description = "Rename the current active session"_s, + .apply = + [](ActionContext const& context) { + context.layout_state.with_lock([&](LayoutState& state) { + for (auto& session : state.active_session()) { + auto [create_pane_args, popup_layout] = Fzf() + .as_text_box() + .with_title("Rename Session"_s) + .with_prompt("Name"_s) + .with_query(session.name().to_owned()) + .popup_args(); + create_pane_args.hooks.did_finish_output = di::make_function( + [&layout_state = context.layout_state, &session, + &render_thread = context.render_thread](di::StringView contents) { + while (contents.ends_with(U'\n')) { + contents = contents.substr(contents.begin(), --contents.end()); + } + if (contents.empty()) { + return; + } + layout_state.with_lock([&](LayoutState&) { + // NOTE: we take the layout state lock to prevent data races. Also, the session is + // guaranteed to still be alive because sessions won't be killed until all their + // panes have exited. And we put the popup in the session. + session.set_name(contents.to_owned()); + }); + render_thread.request_render(); + }); + for (auto& tab : state.active_tab()) { + (void) state.popup_pane(session, tab, popup_layout, di::move(create_pane_args), + context.render_thread); + } + } + }); + context.render_thread.request_render(); + }, + }; +} + +auto switch_next_session() -> Action { + return { + .description = "Switch to the next session by creation order"_s, + .apply = + [](ActionContext const& context) { + context.layout_state.with_lock([&](LayoutState& state) { + for (auto& session : state.active_session()) { + auto sessions = state.sessions() | di::transform([](Session& session) { + return &session; + }); + auto it = di::find(sessions, &session); + if (it == sessions.end()) { + return; + } + auto index = usize(it - sessions.begin()); + index++; + index %= sessions.size(); + state.set_active_session(sessions[isize(index)]); + } + }); + context.render_thread.request_render(); + }, + }; +} + +auto switch_prev_session() -> Action { + return { + .description = "Switch to the previous session by creation order"_s, + .apply = + [](ActionContext const& context) { + context.layout_state.with_lock([&](LayoutState& state) { + for (auto& session : state.active_session()) { + auto sessions = state.sessions() | di::transform([](Session& session) { + return &session; + }); + auto it = di::find(sessions, &session); + if (it == sessions.end()) { + return; + } + auto index = usize(it - sessions.begin()); + index += sessions.size(); + index--; + index %= sessions.size(); + state.set_active_session(sessions[isize(index)]); + } + }); + context.render_thread.request_render(); + }, + }; +} + +auto find_session() -> Action { + return { + .description = "Find a session by name using fzf"_s, + .apply = + [](ActionContext const& context) { + context.layout_state.with_lock([&](LayoutState& state) { + auto session_names = di::Vector(); + for (auto [i, session] : state.sessions() | di::enumerate) { + session_names.push_back(*di::present("{} {}"_sv, i + 1, session.name())); + } + + auto [create_pane_args, popup_layout] = Fzf() + .with_prompt("Switch to session"_s) + .with_title("Sessions"_s) + .with_input(di::move(session_names)) + .popup_args(); + create_pane_args.hooks.did_finish_output = di::make_function( + [&layout_state = context.layout_state, + &render_thread = context.render_thread](di::StringView contents) { + // Try to parse the first index. This will fail if contents is empty. + auto maybe_session_index = di::parse_partial(contents); + if (!maybe_session_index || maybe_session_index == 0) { + return; + } + auto session_index = maybe_session_index.value() - 1; + layout_state.with_lock([&](LayoutState& state) { + if (auto session = state.sessions().at(session_index)) { + state.set_active_session(&session.value()); + } + }); + render_thread.request_render(); + }); + for (auto& session : state.active_session()) { + for (auto& tab : state.active_tab()) { + (void) state.popup_pane(session, tab, popup_layout, di::move(create_pane_args), + context.render_thread); + } + } + }); + context.render_thread.request_render(); + }, + }; +} + auto quit() -> Action { return { .description = "Quit ttx"_s, diff --git a/src/actions.h b/src/actions.h index 1d42b3e..0ad3919 100644 --- a/src/actions.h +++ b/src/actions.h @@ -15,6 +15,11 @@ auto switch_tab(usize index) -> Action; auto switch_next_tab() -> Action; auto switch_prev_tab() -> Action; auto find_tab() -> Action; +auto create_session() -> Action; +auto rename_session() -> Action; +auto switch_next_session() -> Action; +auto switch_prev_session() -> Action; +auto find_session() -> Action; auto quit() -> Action; auto save_state(di::Path path) -> Action; auto stop_capture() -> Action; diff --git a/src/fzf.cpp b/src/fzf.cpp index 34744bf..3768180 100644 --- a/src/fzf.cpp +++ b/src/fzf.cpp @@ -15,7 +15,8 @@ auto Fzf::popup_args() && -> di::Tuple { // Build the command string based on the configuration. For now, most // fields are hard-coded. create_pane_args.command = di::Array { - "fzf"_ts, "--border"_ts, "--layout"_ts, "reverse"_ts, "--info"_ts, "inline-right"_ts, + "fzf"_ts, "--border"_ts, "--layout"_ts, "reverse"_ts, + "--info"_ts, "inline-right"_ts, "--no-multi"_ts, "--cycle"_ts, } | di::to(); if (m_prompt) { create_pane_args.command.push_back("--prompt"_ts); diff --git a/src/key_bind.cpp b/src/key_bind.cpp index 83d141c..b3d284f 100644 --- a/src/key_bind.cpp +++ b/src/key_bind.cpp @@ -197,11 +197,47 @@ auto make_key_binds(Key prefix, di::Path save_state_path, bool replay_mode) -> d .action = toggle_full_screen_pane(), }); result.push_back({ - .key = Key::BackSlash, + .key = Key::C, .modifiers = Modifiers::Shift, .mode = InputMode::Normal, - .action = add_pane(Direction::Horizontal), - }); + .action = create_session(), + }), + result.push_back({ + .key = Key::_4, + .modifiers = Modifiers::Shift, + .mode = InputMode::Normal, + .action = rename_session(), + }), + result.push_back({ + .key = Key::C, + .modifiers = Modifiers::Shift, + .mode = InputMode::Normal, + .action = create_session(), + }), + result.push_back({ + .key = Key::F, + .modifiers = Modifiers::Shift, + .mode = InputMode::Normal, + .action = find_session(), + }), + result.push_back({ + .key = Key::_9, + .modifiers = Modifiers::Shift, + .mode = InputMode::Normal, + .action = switch_prev_session(), + }), + result.push_back({ + .key = Key::_0, + .modifiers = Modifiers::Shift, + .mode = InputMode::Normal, + .action = switch_next_session(), + }), + result.push_back({ + .key = Key::BackSlash, + .modifiers = Modifiers::Shift, + .mode = InputMode::Normal, + .action = add_pane(Direction::Horizontal), + }); result.push_back({ .key = Key::Minus, .mode = InputMode::Normal, From 41b3b10223c27a38665635a1d2385d5674c9934d Mon Sep 17 00:00:00 2001 From: coletrammer Date: Mon, 14 Apr 2025 19:25:17 -0700 Subject: [PATCH 14/19] terminal: fix scroll back when we exceed the maximum scroll back size --- lib/src/terminal/scroll_back.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/terminal/scroll_back.cpp b/lib/src/terminal/scroll_back.cpp index 08a0c60..97dfaa3 100644 --- a/lib/src/terminal/scroll_back.cpp +++ b/lib/src/terminal/scroll_back.cpp @@ -88,7 +88,9 @@ auto ScrollBack::is_last_group_full() const -> bool { auto ScrollBack::add_group() -> Group& { if (m_groups.size() >= max_groups) { - m_absolute_row_start += m_groups.front().value().group.total_rows(); + auto deleted_rows = m_groups.front().value().group.total_rows(); + m_absolute_row_start += deleted_rows; + m_total_rows -= deleted_rows; m_groups.pop_front(); } return m_groups.emplace_back(); From 7b062b5a74650e8b4fcb5eaca51a71cba6b35aca Mon Sep 17 00:00:00 2001 From: coletrammer Date: Mon, 14 Apr 2025 19:32:38 -0700 Subject: [PATCH 15/19] terminal: fix incorrect row width when filling cells from scroll back --- lib/src/terminal/screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/terminal/screen.cpp b/lib/src/terminal/screen.cpp index 9a4c80b..19a25c5 100644 --- a/lib/src/terminal/screen.cpp +++ b/lib/src/terminal/screen.cpp @@ -98,7 +98,7 @@ void Screen::resize(Size const& size) { if (m_scroll_back_enabled == ScrollBackEnabled::Yes && !m_never_got_input) { // When taking rows from the scroll back, we also need to move the cursor down to accomdate the new rows. auto rows_to_take = di::min(m_scroll_back.total_rows(), size.rows - rows().size()); - m_scroll_back.take_rows(m_active_rows, max_width(), 0, rows_to_take); + m_scroll_back.take_rows(m_active_rows, size.cols, 0, rows_to_take); m_cursor.row += rows_to_take; // In this case, we may need to adjust the visual scroll offset. From ac897fe2f9204ffb9ae81bdaf263fe233d91fa17 Mon Sep 17 00:00:00 2001 From: coletrammer Date: Mon, 14 Apr 2025 19:38:40 -0700 Subject: [PATCH 16/19] client: prevent having more than 1 popup --- src/tab.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tab.cpp b/src/tab.cpp index 8f23e37..3241fb9 100644 --- a/src/tab.cpp +++ b/src/tab.cpp @@ -89,6 +89,10 @@ auto Tab::add_pane(u64 pane_id, Size const& size, CreatePaneArgs args, Direction auto Tab::popup_pane(u64 pane_id, PopupLayout const& popup_layout, Size const& size, CreatePaneArgs args, RenderThread& render_thread) -> di::Result<> { + // Prevent creating more than 1 popup. + if (m_popup) { + return di::Unexpected(di::BasicError::InvalidArgument); + } m_popup = Popup { .pane = nullptr, .layout_config = popup_layout, From 20a210138a8e4aef3c1e68869813e2c5167e728a Mon Sep 17 00:00:00 2001 From: coletrammer Date: Mon, 14 Apr 2025 22:34:10 -0700 Subject: [PATCH 17/19] client: perform terminal setup on every SIGWINCH This is a temporary measure to enable running ttx in a program like dtach. Without this, all of the terminal modes ttx sets will be lost when reattaching to the session. Once ttx supports a proper client/server architecture, this logic can be removed. --- lib/include/ttx/renderer.h | 5 +++++ lib/src/renderer.cpp | 43 ++++++++++++++++++++++++++++++++++++++ src/render.cpp | 15 +++++++++++++ src/ttx.cpp | 38 +-------------------------------- 4 files changed, 64 insertions(+), 37 deletions(-) diff --git a/lib/include/ttx/renderer.h b/lib/include/ttx/renderer.h index 8b23334..f0cc0c7 100644 --- a/lib/include/ttx/renderer.h +++ b/lib/include/ttx/renderer.h @@ -18,6 +18,9 @@ struct RenderedCursor { class Renderer { public: + auto setup(dius::SyncFile& output) -> di::Result<>; + auto cleanup(dius::SyncFile& output) -> di::Result<>; + void start(Size const& size); auto finish(dius::SyncFile& output, RenderedCursor const& cursor) -> di::Result<>; @@ -42,6 +45,8 @@ class Renderer { di::VectorWriter<> m_buffer; Size m_size; + di::Vector m_cleanup; + GraphicsRendition m_last_graphics_rendition; di::Optional m_last_hyperlink; u32 m_last_cursor_row { 0 }; diff --git a/lib/src/renderer.cpp b/lib/src/renderer.cpp index aee2cd6..041b7dc 100644 --- a/lib/src/renderer.cpp +++ b/lib/src/renderer.cpp @@ -9,6 +9,49 @@ #include "ttx/terminal/escapes/osc_8.h" namespace ttx { +auto Renderer::setup(dius::SyncFile& output) -> di::Result<> { + m_cleanup = {}; + + // Setup - alternate screen buffer. + di::writer_print(output, "\033[?1049h\033[H\033[2J"_sv); + m_cleanup.push_back("\033[?1049l\033[?25h"_s); + + // Setup - disable autowrap. + di::writer_print(output, "\033[?7l"_sv); + m_cleanup.push_back("\033[?7h"_s); + + // Setup - kitty key mode. + di::writer_print(output, "\033[>31u"_sv); + m_cleanup.push_back("\033[(output, "\033[?1003h\033[?1006h"_sv); + m_cleanup.push_back("\033[?1006l\033[?1003l"_s); + + // Setup - enable focus events. + di::writer_print(output, "\033[?1004h"_sv); + m_cleanup.push_back("\033[?1004l"_s); + + // Setup - bracketed paste. + di::writer_print(output, "\033[?2004h"_sv); + m_cleanup.push_back("\033[?2004l"_s); + + auto text = di::move(m_buffer).vector(); + m_buffer = {}; + return output.write_exactly(di::as_bytes(text.span())); +} + +auto Renderer::cleanup(dius::SyncFile& output) -> di::Result<> { + for (auto const& string : m_cleanup | di::reverse) { + di::writer_print(m_buffer, "{}"_sv, string); + } + m_cleanup.clear(); + + auto text = di::move(m_buffer).vector(); + m_buffer = {}; + return output.write_exactly(di::as_bytes(text.span())); +} + void Renderer::start(Size const& size) { m_buffer = {}; m_size = size; diff --git a/src/render.cpp b/src/render.cpp index 2ea7fea..98c4c26 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -35,8 +35,12 @@ void RenderThread::render_thread() { }); auto renderer = Renderer(); + auto _ = di::ScopeExit([&] { + (void) renderer.cleanup(dius::stdin); + }); auto deadline = dius::SteadyClock::now(); + auto do_setup = true; for (;;) { while (deadline < dius::SteadyClock::now()) { deadline += di::Milliseconds(25); // 50 FPS @@ -65,6 +69,11 @@ void RenderThread::render_thread() { if (auto ev = di::get_if(event)) { // Do layout. m_layout_state.lock()->layout(ev.value()); + + // Force doing a resetting the terminal mode on SIGWINCH. + // This is to enable make putting ttx in a "dumb" session + // persistence program (like dtach) work correctly. + do_setup = true; } else if (auto ev = di::get_if(event)) { // Exit pane. auto [pane, should_exit] = m_layout_state.with_lock([&](LayoutState& state) { @@ -86,6 +95,12 @@ void RenderThread::render_thread() { } } + // Do terminal setup if requested. + if (do_setup) { + (void) renderer.setup(dius::stdin); + do_setup = false; + } + // Do render. do_render(renderer); } diff --git a/src/ttx.cpp b/src/ttx.cpp index a7d81a5..040c888 100644 --- a/src/ttx.cpp +++ b/src/ttx.cpp @@ -69,49 +69,13 @@ static auto main(Args& args) -> di::Result { } // Setup - initial state and terminal size. - auto initial_size = args.headless ? Size { 25, 80, 25 * 16, 80 * 16 } + auto initial_size = args.headless ? Size { 24, 80, 24 * 16, 80 * 16 } : Size::from_window_size(TRY(dius::stdin.get_tty_window_size())); auto layout_state = di::Synchronized(LayoutState(initial_size, args.hide_status_bar)); // Setup - raw mode auto _ = args.headless ? di::ScopeExit(di::Function([] {})) : TRY(dius::stdin.enter_raw_mode()); - // Setup - alternate screen buffer. - di::writer_print(dius::stdin, "\033[?1049h\033[H\033[2J"_sv); - auto _ = di::ScopeExit([&] { - di::writer_print(dius::stdin, "\033[?1049l\033[?25h"_sv); - }); - - // Setup - disable autowrap. - di::writer_print(dius::stdin, "\033[?7l"_sv); - auto _ = di::ScopeExit([&] { - di::writer_print(dius::stdin, "\033[?7h"_sv); - }); - - // Setup - kitty key mode. - di::writer_print(dius::stdin, "\033[>31u"_sv); - auto _ = di::ScopeExit([&] { - di::writer_print(dius::stdin, "\033[(dius::stdin, "\033[?1003h\033[?1006h"_sv); - auto _ = di::ScopeExit([&] { - di::writer_print(dius::stdin, "\033[?1006l\033[?1003l"_sv); - }); - - // Setup - enable focus events. - di::writer_print(dius::stdin, "\033[?1004h"_sv); - auto _ = di::ScopeExit([&] { - di::writer_print(dius::stdin, "\033[?1004l"_sv); - }); - - // Setup - bracketed paste. - di::writer_print(dius::stdin, "\033[?2004h"_sv); - auto _ = di::ScopeExit([&] { - di::writer_print(dius::stdin, "\033[?2004l"_sv); - }); - // Setup - block SIGWINCH. TRY(dius::system::mask_signal(dius::Signal::WindowChange)); From f1b7d854f9e073d7302c058cc482220640366d0c Mon Sep 17 00:00:00 2001 From: coletrammer Date: Mon, 14 Apr 2025 22:44:42 -0700 Subject: [PATCH 18/19] terminal: make kitty keyboard flags per screen The spec is clear on this, but I forgot to put the state in the ScreenState struct. This helps ensure we clear the kitty key flags when exiting the alternate screen buffer. --- lib/include/ttx/terminal.h | 9 ++- lib/src/terminal.cpp | 73 +++++++++++-------- .../cases/git-log-with-scroll.expected.ansi | 4 +- lib/test/cases/git-status.expected.ansi | 4 +- lib/test/cases/hyperlink-demo.expected.ansi | 9 +-- .../cases/nix-output-monitor.expected.ansi | 4 +- lib/test/cases/nvim-basic.expected.ansi | 6 +- lib/test/cases/vttest-1-1.expected.ansi | 2 +- lib/test/cases/vttest-1-2.expected.ansi | 2 +- lib/test/cases/vttest-1-3.expected.ansi | 2 +- lib/test/cases/vttest-1-4.expected.ansi | 2 +- lib/test/cases/vttest-1-5.expected.ansi | 2 +- lib/test/cases/vttest-1-6.expected.ansi | 2 +- lib/test/cases/vttest-11-1-2-3.expected.ansi | 2 +- lib/test/cases/vttest-11-7-2.expected.ansi | 2 +- lib/test/cases/vttest-2-1.expected.ansi | 2 +- lib/test/cases/vttest-2-10.expected.ansi | 2 +- lib/test/cases/vttest-2-11.expected.ansi | 2 +- lib/test/cases/vttest-2-12.expected.ansi | 2 +- lib/test/cases/vttest-2-13.expected.ansi | 2 +- lib/test/cases/vttest-2-14.expected.ansi | 4 +- lib/test/cases/vttest-2-15.expected.ansi | 2 +- lib/test/cases/vttest-2-2.expected.ansi | 2 +- lib/test/cases/vttest-2-3.expected.ansi | 4 +- lib/test/cases/vttest-2-4.expected.ansi | 4 +- lib/test/cases/vttest-2-5.expected.ansi | 2 +- lib/test/cases/vttest-2-6.expected.ansi | 2 +- lib/test/cases/vttest-2-7.expected.ansi | 2 +- lib/test/cases/vttest-2-8.expected.ansi | 2 +- lib/test/cases/vttest-2-9.expected.ansi | 2 +- lib/test/cases/zsh-eza.expected.ansi | 4 +- 31 files changed, 88 insertions(+), 77 deletions(-) diff --git a/lib/include/ttx/terminal.h b/lib/include/ttx/terminal.h index 4e6f011..8d83331 100644 --- a/lib/include/ttx/terminal.h +++ b/lib/include/ttx/terminal.h @@ -28,6 +28,11 @@ class Terminal { terminal::Screen screen; di::Optional saved_cursor; CursorStyle cursor_style { CursorStyle::SteadyBar }; + + // Per https://sw.kovidgoyal.net/kitty/keyboard-protocol/#detection-of-support-for-this-protocol, + // the keyboard mode stack and flags are per-screen. + KeyReportingFlags m_key_reporting_flags { KeyReportingFlags::None }; + di::Ring m_key_reporting_flags_stack; }; public: @@ -64,7 +69,7 @@ class Terminal { auto visible_size() const -> Size { return m_available_size; } auto application_cursor_keys_mode() const -> ApplicationCursorKeysMode { return m_application_cursor_keys_mode; } - auto key_reporting_flags() const -> KeyReportingFlags { return m_key_reporting_flags; } + auto key_reporting_flags() const -> KeyReportingFlags { return active_screen().m_key_reporting_flags; } auto alternate_scroll_mode() const -> AlternateScrollMode { return m_alternate_scroll_mode; } auto mouse_protocol() const -> MouseProtocol { return m_mouse_protocol; } @@ -192,8 +197,6 @@ class Terminal { di::Optional m_last_graphics_charcter { 0 }; ApplicationCursorKeysMode m_application_cursor_keys_mode { ApplicationCursorKeysMode::Disabled }; - KeyReportingFlags m_key_reporting_flags { KeyReportingFlags::None }; - di::Ring m_key_reporting_flags_stack; AlternateScrollMode m_alternate_scroll_mode { AlternateScrollMode::Disabled }; MouseProtocol m_mouse_protocol { MouseProtocol::None }; diff --git a/lib/src/terminal.cpp b/lib/src/terminal.cpp index 07ef2a6..d4e1b21 100644 --- a/lib/src/terminal.cpp +++ b/lib/src/terminal.cpp @@ -1059,16 +1059,17 @@ void Terminal::csi_set_key_reporting_flags(Params const& params) { auto flags_u32 = params.get(0); auto mode = params.get(1, 1); + auto& screen = active_screen(); auto flags = KeyReportingFlags(flags_u32) & KeyReportingFlags::All; switch (mode) { case 1: - m_key_reporting_flags = flags; + screen.m_key_reporting_flags = flags; break; case 2: - m_key_reporting_flags |= flags; + screen.m_key_reporting_flags |= flags; break; case 3: - m_key_reporting_flags &= ~flags; + screen.m_key_reporting_flags &= ~flags; break; default: break; @@ -1078,7 +1079,7 @@ void Terminal::csi_set_key_reporting_flags(Params const& params) { // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement void Terminal::csi_get_key_reporting_flags(Params const&) { (void) m_psuedo_terminal.write_exactly( - di::as_bytes(di::present("\033[?{}u"_sv, u32(m_key_reporting_flags)).value().span())); + di::as_bytes(di::present("\033[?{}u"_sv, u32(active_screen().m_key_reporting_flags)).value().span())); } // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement @@ -1086,11 +1087,12 @@ void Terminal::csi_push_key_reporting_flags(Params const& params) { auto flags_u32 = params.get(0); auto flags = KeyReportingFlags(flags_u32) & KeyReportingFlags::All; - if (m_key_reporting_flags_stack.size() >= 100) { - m_key_reporting_flags_stack.pop_front(); + auto& screen = active_screen(); + if (screen.m_key_reporting_flags_stack.size() >= 100) { + screen.m_key_reporting_flags_stack.pop_front(); } - m_key_reporting_flags_stack.push_back(m_key_reporting_flags); - m_key_reporting_flags = flags; + screen.m_key_reporting_flags_stack.push_back(screen.m_key_reporting_flags); + screen.m_key_reporting_flags = flags; } // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement @@ -1099,15 +1101,17 @@ void Terminal::csi_pop_key_reporting_flags(Params const& params) { if (n == 0) { return; } - if (n >= m_key_reporting_flags_stack.size()) { - m_key_reporting_flags_stack.clear(); - m_key_reporting_flags = KeyReportingFlags::None; + + auto& screen = active_screen(); + if (n >= screen.m_key_reporting_flags_stack.size()) { + screen.m_key_reporting_flags_stack.clear(); + screen.m_key_reporting_flags = KeyReportingFlags::None; return; } - auto new_stack_size = m_key_reporting_flags_stack.size() - n; - m_key_reporting_flags = m_key_reporting_flags_stack[new_stack_size]; - m_key_reporting_flags_stack.erase(m_key_reporting_flags_stack.begin() + isize(new_stack_size)); + auto new_stack_size = screen.m_key_reporting_flags_stack.size() - n; + screen.m_key_reporting_flags = screen.m_key_reporting_flags_stack[new_stack_size]; + screen.m_key_reporting_flags_stack.erase(screen.m_key_reporting_flags_stack.begin() + isize(new_stack_size)); } void Terminal::set_visible_size(Size const& size) { @@ -1190,8 +1194,9 @@ void Terminal::soft_reset() { m_auto_wrap_mode = terminal::AutoWrapMode::Enabled; m_mouse_encoding = MouseEncoding::X10; m_mouse_protocol = MouseProtocol::None; - m_key_reporting_flags_stack.clear(); - m_key_reporting_flags = KeyReportingFlags::None; + active_screen().m_key_reporting_flags_stack.clear(); + active_screen().m_key_reporting_flags = KeyReportingFlags::None; + m_focus_event_mode = FocusEventMode::Disabled; m_cursor_hidden = false; m_disable_drawing = false; @@ -1233,10 +1238,30 @@ auto Terminal::state_as_escape_sequences() const -> di::String { di::writer_print(writer, "\033[{} q"_sv, i32(screen.cursor_style)); }; + auto kitty_key_flags = [&](ScreenState const& screen) { + // Kitty key flags + auto first = true; + auto set_kitty_key_flags = [&](KeyReportingFlags flags) { + if (first) { + di::writer_print(writer, "\033[=1;{}u"_sv, i32(flags)); + first = false; + } else { + di::writer_print(writer, "\033[>{}u"_sv, i32(flags)); + } + }; + + for (auto flags : screen.m_key_reporting_flags_stack) { + set_kitty_key_flags(flags); + } + set_kitty_key_flags(screen.m_key_reporting_flags); + }; + print_screen(m_primary_screen); + kitty_key_flags(m_primary_screen); if (m_alternate_screen) { di::writer_print(writer, "\033[?1049h\033[H\033[2J"_sv); print_screen(*m_alternate_screen); + kitty_key_flags(*m_alternate_screen); } } @@ -1265,22 +1290,6 @@ auto Terminal::state_as_escape_sequences() const -> di::String { di::writer_print(writer, "\033[?25l"_sv); } - // Kitty key flags - auto first = true; - auto set_kitty_key_flags = [&](KeyReportingFlags flags) { - if (first) { - di::writer_print(writer, "\033[=1;{}u"_sv, i32(flags)); - first = false; - } else { - di::writer_print(writer, "\033[>{}u"_sv, i32(flags)); - } - }; - - for (auto flags : m_key_reporting_flags_stack) { - set_kitty_key_flags(flags); - } - set_kitty_key_flags(m_key_reporting_flags); - // Alternate scroll mode if (m_alternate_scroll_mode == AlternateScrollMode::Enabled) { di::writer_print(writer, "\033[?1007h"_sv); diff --git a/lib/test/cases/git-log-with-scroll.expected.ansi b/lib/test/cases/git-log-with-scroll.expected.ansi index 6dd6a93..448f20f 100644 --- a/lib/test/cases/git-log-with-scroll.expected.ansi +++ b/lib/test/cases/git-log-with-scroll.expected.ansi @@ -1,4 +1,4 @@ -c[?7h❯ git log +c[?7h❯ git log ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ commit ]8;id=1;https://github.com/ColeTrammer/ttx/commit/c5f418c23f45c2b372ac88fd249ce45f79ba9853\c5f418c23f45c2b372ac88fd249ce45f79ba9853]8;;\ (HEAD -> testing, origin/testing) ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -40,4 +40,4 @@ Date: Sat Mar 22 22:02:26 2025 -0700 Date: Sat Mar 22 19:12:19 2025 -0700  meta: fix clang tidy violations -:78[1 q[?1h[=1;0u \ No newline at end of file +:78[1 q[=1;0u[?1h \ No newline at end of file diff --git a/lib/test/cases/git-status.expected.ansi b/lib/test/cases/git-status.expected.ansi index 71d496e..480b666 100644 --- a/lib/test/cases/git-status.expected.ansi +++ b/lib/test/cases/git-status.expected.ansi @@ -1,4 +1,4 @@ -c[?7h +c[?7h @@ -118,4 +118,4 @@ Changes not staged for commit:[    -8[6 q[?1h[=1;0u[?2004h \ No newline at end of file +8[6 q[=1;0u[?1h[?2004h \ No newline at end of file diff --git a/lib/test/cases/hyperlink-demo.expected.ansi b/lib/test/cases/hyperlink-demo.expected.ansi index e3e9814..64c61b7 100644 --- a/lib/test/cases/hyperlink-demo.expected.ansi +++ b/lib/test/cases/hyperlink-demo.expected.ansi @@ -1,4 +1,4 @@ -c[?7hTests for ]8;id=1;https://bugzilla.gnome.org/show_bug.cgi?id=779734\gnome-terminal #779734]8;;\ and ]8;id=2;https://gitlab.com/gnachman/iterm2/issues/5158\iTerm2 #5158 +c[?7hTests for ]8;id=1;https://bugzilla.gnome.org/show_bug.cgi?id=779734\gnome-terminal #779734]8;;\ and ]8;id=2;https://gitlab.com/gnachman/iterm2/issues/5158\iTerm2 #5158 ]8;;\═════════════════════════════════════════════════  commit ]8;id=3;https://git.gnome.org/browse/vte/commit/?id=a9b0b4c75a6dc7282f7cfcaef71413d69f7f0731\a9b0b4c75a6dc7282f7cfcaef71413d69f7f0731 @@ -48,6 +48,7 @@ Explicit and implicit link: ]8;id=46;http://example.com/under_score\http://exa ]8;;\Explicit and implicit link, broken into two lines: ]8;id=48;http://example.com/under_score\http://examp le.com/under_score  +]8;;\Explicitly underlined links ("\e[4m"): @@ -71,9 +72,7 @@ le.com/under_score - -]8;;\Explicitly underlined links ("\e[4m"): -Explicit link only: ]8;id=49;http://example.com/under_score\I'm an explicit link with under_score +Explicit link only: ]8;id=49;http://example.com/under_score\I'm an explicit link with under_score ]8;;\Implicit link only: http://example.com/under_score Both: ]8;id=50;http://example.com/under_score\http://example.com/under_score  @@ -96,4 +95,4 @@ Invisible implicit link: «http://example.com/how_about_me» ]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230;http://example.com/id\ID of 250 bytes once,]8;;\ ]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230;http://example.com/id\twice ID of 251 bytes once,]8;;\ ]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230;http://example.com/id\twice  -78]8;;\[2 q[=1;0u \ No newline at end of file +78]8;;\[2 q[=1;0u \ No newline at end of file diff --git a/lib/test/cases/nix-output-monitor.expected.ansi b/lib/test/cases/nix-output-monitor.expected.ansi index 1fe21d7..0282854 100644 --- a/lib/test/cases/nix-output-monitor.expected.ansi +++ b/lib/test/cases/nix-output-monitor.expected.ansi @@ -1,4 +1,4 @@ -c[?7h +c[?7h @@ -112,4 +112,4 @@ ttx-lib-dius-runtime> -- Build files have been written to: /build/bxdc05nah9    -8[6 q[?25l[=1;0u \ No newline at end of file +8[6 q[=1;0u[?25l \ No newline at end of file diff --git a/lib/test/cases/nvim-basic.expected.ansi b/lib/test/cases/nvim-basic.expected.ansi index e6a7f6b..97abdba 100644 --- a/lib/test/cases/nvim-basic.expected.ansi +++ b/lib/test/cases/nvim-basic.expected.ansi @@ -1,4 +1,4 @@ -c[?7h +c[?7h @@ -28,7 +28,7 @@    -8[1 q[?1049h +8[1 q[=1;0u[?1049h @@ -58,4 +58,4 @@  2 │ 11 #include "ttx/mouse_event.h"  3 # Show output via ttx│ 12 #include "ttx/paste_event.h"  4 "$TTX_BUILD_DIR/ttx" -r "$expected"│ 13 #include "ttx/terminal_input.h"  1/8  (12%) ⠇ indexing clangd - NORMAL   testing  󰙲 input.cpp gj  utf-8 |   Top 1:1 8[2 q[?1h[=1;0u[>1u[?1002h[?1006h[?1004h[?2004h \ No newline at end of file + NORMAL   testing  󰙲 input.cpp gj  utf-8 |   Top 1:1 8[2 q[=1;0u[>1u[?1h[?1002h[?1006h[?1004h[?2004h \ No newline at end of file diff --git a/lib/test/cases/vttest-1-1.expected.ansi b/lib/test/cases/vttest-1-1.expected.ansi index 4740820..7b8a73f 100644 --- a/lib/test/cases/vttest-1-1.expected.ansi +++ b/lib/test/cases/vttest-1-1.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3l[?7h +c[?40h[?3l[?7h    diff --git a/lib/test/cases/vttest-1-2.expected.ansi b/lib/test/cases/vttest-1-2.expected.ansi index 234cafe..9720d0e 100644 --- a/lib/test/cases/vttest-1-2.expected.ansi +++ b/lib/test/cases/vttest-1-2.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3h[?7h +c[?40h[?3h[?7h diff --git a/lib/test/cases/vttest-1-3.expected.ansi b/lib/test/cases/vttest-1-3.expected.ansi index 41a6981..0763250 100644 --- a/lib/test/cases/vttest-1-3.expected.ansi +++ b/lib/test/cases/vttest-1-3.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3l[?7h +c[?40h[?3l[?7h    diff --git a/lib/test/cases/vttest-1-4.expected.ansi b/lib/test/cases/vttest-1-4.expected.ansi index 0753477..f7d1778 100644 --- a/lib/test/cases/vttest-1-4.expected.ansi +++ b/lib/test/cases/vttest-1-4.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3h[?7h +c[?40h[?3h[?7h    diff --git a/lib/test/cases/vttest-1-5.expected.ansi b/lib/test/cases/vttest-1-5.expected.ansi index 6bfebc7..c98bb8e 100644 --- a/lib/test/cases/vttest-1-5.expected.ansi +++ b/lib/test/cases/vttest-1-5.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3l[?7h +c[?40h[?3l[?7h diff --git a/lib/test/cases/vttest-1-6.expected.ansi b/lib/test/cases/vttest-1-6.expected.ansi index 92caf67..565df2a 100644 --- a/lib/test/cases/vttest-1-6.expected.ansi +++ b/lib/test/cases/vttest-1-6.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3l[?7h +c[?40h[?3l[?7h diff --git a/lib/test/cases/vttest-11-1-2-3.expected.ansi b/lib/test/cases/vttest-11-1-2-3.expected.ansi index 0d7330b..434b943 100644 --- a/lib/test/cases/vttest-11-1-2-3.expected.ansi +++ b/lib/test/cases/vttest-11-1-2-3.expected.ansi @@ -1,4 +1,4 @@ -c[?7h +c[?7h diff --git a/lib/test/cases/vttest-11-7-2.expected.ansi b/lib/test/cases/vttest-11-7-2.expected.ansi index 97e64ab..359e8f4 100644 --- a/lib/test/cases/vttest-11-7-2.expected.ansi +++ b/lib/test/cases/vttest-11-7-2.expected.ansi @@ -1,4 +1,4 @@ -c[?7h +c[?7h diff --git a/lib/test/cases/vttest-2-1.expected.ansi b/lib/test/cases/vttest-2-1.expected.ansi index 01f2340..bd519af 100644 --- a/lib/test/cases/vttest-2-1.expected.ansi +++ b/lib/test/cases/vttest-2-1.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3l[?7h +c[?40h[?3l[?7h diff --git a/lib/test/cases/vttest-2-10.expected.ansi b/lib/test/cases/vttest-2-10.expected.ansi index 8bc0c25..f7e5bb4 100644 --- a/lib/test/cases/vttest-2-10.expected.ansi +++ b/lib/test/cases/vttest-2-10.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h[?6h +c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h[?6h Soft scroll up region [12..13] size 2 Line 1 Soft scroll up region [12..13] size 2 Line 2 Soft scroll up region [12..13] size 2 Line 3 diff --git a/lib/test/cases/vttest-2-11.expected.ansi b/lib/test/cases/vttest-2-11.expected.ansi index b89e3e9..16486b9 100644 --- a/lib/test/cases/vttest-2-11.expected.ansi +++ b/lib/test/cases/vttest-2-11.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h[?6h +c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h[?6h Soft scroll up region [12..13] size 2 Line 1 Soft scroll up region [12..13] size 2 Line 2 Soft scroll up region [12..13] size 2 Line 3 diff --git a/lib/test/cases/vttest-2-12.expected.ansi b/lib/test/cases/vttest-2-12.expected.ansi index 8b8fb51..d1c120c 100644 --- a/lib/test/cases/vttest-2-12.expected.ansi +++ b/lib/test/cases/vttest-2-12.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h +c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h Soft scroll up region [12..13] size 2 Line 1 Soft scroll up region [12..13] size 2 Line 2 Soft scroll up region [12..13] size 2 Line 3 diff --git a/lib/test/cases/vttest-2-13.expected.ansi b/lib/test/cases/vttest-2-13.expected.ansi index 97b8ab3..e07c2ef 100644 --- a/lib/test/cases/vttest-2-13.expected.ansi +++ b/lib/test/cases/vttest-2-13.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h +c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h Soft scroll up region [12..13] size 2 Line 1 Soft scroll up region [12..13] size 2 Line 2 Soft scroll up region [12..13] size 2 Line 3 diff --git a/lib/test/cases/vttest-2-14.expected.ansi b/lib/test/cases/vttest-2-14.expected.ansi index 7839374..d89528d 100644 --- a/lib/test/cases/vttest-2-14.expected.ansi +++ b/lib/test/cases/vttest-2-14.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h +c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h Soft scroll up region [12..13] size 2 Line 1 Soft scroll up region [12..13] size 2 Line 2 Soft scroll up region [12..13] size 2 Line 3 @@ -160,4 +160,4 @@ vanilla   Light background. Push 7 -8[6 q[?5h[=1;0u \ No newline at end of file +8[6 q[=1;0u[?5h \ No newline at end of file diff --git a/lib/test/cases/vttest-2-15.expected.ansi b/lib/test/cases/vttest-2-15.expected.ansi index c0bd4f9..433e82d 100644 --- a/lib/test/cases/vttest-2-15.expected.ansi +++ b/lib/test/cases/vttest-2-15.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h +c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h Soft scroll up region [12..13] size 2 Line 1 Soft scroll up region [12..13] size 2 Line 2 Soft scroll up region [12..13] size 2 Line 3 diff --git a/lib/test/cases/vttest-2-2.expected.ansi b/lib/test/cases/vttest-2-2.expected.ansi index aab8462..87be5e4 100644 --- a/lib/test/cases/vttest-2-2.expected.ansi +++ b/lib/test/cases/vttest-2-2.expected.ansi @@ -1,4 +1,4 @@ -cHHHHHHHHHHHHH[?7h +cHHHHHHHHHHHHH[?7h diff --git a/lib/test/cases/vttest-2-3.expected.ansi b/lib/test/cases/vttest-2-3.expected.ansi index 9c25334..5385fdf 100644 --- a/lib/test/cases/vttest-2-3.expected.ansi +++ b/lib/test/cases/vttest-2-3.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3hHHHHHHHHHHHHHHHHH[?7h +c[?40h[?3hHHHHHHHHHHHHHHHHH[?7h    @@ -81,4 +81,4 @@    -8[6 q[?5h[=1;0u \ No newline at end of file +8[6 q[=1;0u[?5h \ No newline at end of file diff --git a/lib/test/cases/vttest-2-4.expected.ansi b/lib/test/cases/vttest-2-4.expected.ansi index 632bc0a..3cd6973 100644 --- a/lib/test/cases/vttest-2-4.expected.ansi +++ b/lib/test/cases/vttest-2-4.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h +c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h @@ -44,4 +44,4 @@    -8[6 q[?5h[=1;0u \ No newline at end of file +8[6 q[=1;0u[?5h \ No newline at end of file diff --git a/lib/test/cases/vttest-2-5.expected.ansi b/lib/test/cases/vttest-2-5.expected.ansi index 3a2050d..cede144 100644 --- a/lib/test/cases/vttest-2-5.expected.ansi +++ b/lib/test/cases/vttest-2-5.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3hHHHHHHHHHHHHHHHHH[?7h +c[?40h[?3hHHHHHHHHHHHHHHHHH[?7h diff --git a/lib/test/cases/vttest-2-6.expected.ansi b/lib/test/cases/vttest-2-6.expected.ansi index 5b91d75..56bff15 100644 --- a/lib/test/cases/vttest-2-6.expected.ansi +++ b/lib/test/cases/vttest-2-6.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h +c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h diff --git a/lib/test/cases/vttest-2-7.expected.ansi b/lib/test/cases/vttest-2-7.expected.ansi index 77c6902..af59d6f 100644 --- a/lib/test/cases/vttest-2-7.expected.ansi +++ b/lib/test/cases/vttest-2-7.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h[?6h +c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h[?6h Soft scroll up region [12..13] size 2 Line 1 Soft scroll up region [12..13] size 2 Line 2 Soft scroll up region [12..13] size 2 Line 3 diff --git a/lib/test/cases/vttest-2-8.expected.ansi b/lib/test/cases/vttest-2-8.expected.ansi index c005368..1486d38 100644 --- a/lib/test/cases/vttest-2-8.expected.ansi +++ b/lib/test/cases/vttest-2-8.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h[?6h +c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h[?6h Soft scroll up region [12..13] size 2 Line 1 Soft scroll up region [12..13] size 2 Line 2 Soft scroll up region [12..13] size 2 Line 3 diff --git a/lib/test/cases/vttest-2-9.expected.ansi b/lib/test/cases/vttest-2-9.expected.ansi index 688c4bc..3e1b933 100644 --- a/lib/test/cases/vttest-2-9.expected.ansi +++ b/lib/test/cases/vttest-2-9.expected.ansi @@ -1,4 +1,4 @@ -c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h[?6h +c[?40h[?3lHHHHHHHHHHHHHHHHH[?7h[?6h Soft scroll up region [12..13] size 2 Line 1 Soft scroll up region [12..13] size 2 Line 2 Soft scroll up region [12..13] size 2 Line 3 diff --git a/lib/test/cases/zsh-eza.expected.ansi b/lib/test/cases/zsh-eza.expected.ansi index 52732a8..800bc10 100644 --- a/lib/test/cases/zsh-eza.expected.ansi +++ b/lib/test/cases/zsh-eza.expected.ansi @@ -1,4 +1,4 @@ -c[?7h❯ ls -l +c[?7h❯ ls -l Permissions Size User Date Modified Git Name drwxr-xr-x - colet 5 seconds -I  ]8;id=1;file:///persist/home/Workspace/cpp/ttx/build\build @@ -31,4 +31,4 @@ drwxr-xr ]8;;\.rw-r--r-- 5.8k colet 56 minutes -M  ]8;id=14;file:///persist/home/Workspace/cpp/ttx/justfile\justfile ]8;;\.rw-r--r-- 1.3k colet 2 weeks --  ]8;id=15;file:///persist/home/Workspace/cpp/ttx/LICENSE\LICENSE ]8;;\.rw-r--r-- 2.6k colet 5 days -- 󰂺 ]8;id=16;file:///persist/home/Workspace/cpp/ttx/README.md\README.md -]8;;\❯ 7 ttx on  testing8[6 q[?1h[=1;0u[?2004h \ No newline at end of file +]8;;\❯ 7 ttx on  testing8[6 q[=1;0u[?1h[?2004h \ No newline at end of file From 784f3bc12d0d68f12dc58fc6dcaeedab5465ef41 Mon Sep 17 00:00:00 2001 From: coletrammer Date: Tue, 15 Apr 2025 21:50:19 -0700 Subject: [PATCH 19/19] meta: fix clang tidy violations --- justfile | 6 +++--- src/actions.cpp | 30 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/justfile b/justfile index c54612c..619974f 100644 --- a/justfile +++ b/justfile @@ -131,7 +131,7 @@ tidy *args="": ensure_configured #!/usr/bin/env bash set -euo pipefail - export ttx_TIDY_ARGS="{{ args }}" + export TTX_TIDY_ARGS="{{ args }}" cmake --build --preset {{ preset }} -t tidy # Run static analysis @@ -139,7 +139,7 @@ analyze *args="": ensure_configured #!/usr/bin/env bash set -euo pipefail - export ttx_TIDY_ARGS="{{ args }}" + export TTX_TIDY_ARGS="{{ args }}" cmake --build --preset {{ preset }} -t analyze # Run clang-tidy and output failures @@ -147,7 +147,7 @@ check_tidy *args="": ensure_configured #!/usr/bin/env bash set -euo pipefail - export ttx_TIDY_ARGS="{{ args }}" + export TTX_TIDY_ARGS="{{ args }}" cmake --build --preset {{ preset }} -t check_tidy # Clean diff --git a/src/actions.cpp b/src/actions.cpp index 3344ba4..013f3ce 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -120,8 +120,8 @@ auto rename_tab() -> Action { }); render_thread.request_render(); }); - for (auto& tab : state.active_tab()) { - (void) state.popup_pane(session, tab, popup_layout, di::move(create_pane_args), + if (auto tab = state.active_tab()) { + (void) state.popup_pane(session, tab.value(), popup_layout, di::move(create_pane_args), context.render_thread); } } @@ -207,8 +207,8 @@ auto find_tab() -> Action { [](ActionContext const& context) { context.layout_state.with_lock([&](LayoutState& state) { auto tab_names = di::Vector(); - for (auto& session : state.active_session()) { - for (auto [i, tab] : session.tabs() | di::enumerate) { + if (auto session = state.active_session()) { + for (auto [i, tab] : session.value().tabs() | di::enumerate) { tab_names.push_back(*di::present("{} {}"_sv, i + 1, tab->name())); } @@ -227,15 +227,15 @@ auto find_tab() -> Action { } auto tab_index = maybe_tab_index.value() - 1; layout_state.with_lock([&](LayoutState& state) { - if (auto tab = session.tabs().at(tab_index)) { - state.set_active_tab(session, tab.value().get()); + if (auto tab = session.value().tabs().at(tab_index)) { + state.set_active_tab(session.value(), tab.value().get()); } }); render_thread.request_render(); }); - for (auto& tab : state.active_tab()) { - (void) state.popup_pane(session, tab, popup_layout, di::move(create_pane_args), - context.render_thread); + if (auto tab = state.active_tab()) { + (void) state.popup_pane(session.value(), tab.value(), popup_layout, + di::move(create_pane_args), context.render_thread); } } }); @@ -287,8 +287,8 @@ auto rename_session() -> Action { }); render_thread.request_render(); }); - for (auto& tab : state.active_tab()) { - (void) state.popup_pane(session, tab, popup_layout, di::move(create_pane_args), + if (auto tab = state.active_tab()) { + (void) state.popup_pane(session, tab.value(), popup_layout, di::move(create_pane_args), context.render_thread); } } @@ -381,10 +381,10 @@ auto find_session() -> Action { }); render_thread.request_render(); }); - for (auto& session : state.active_session()) { - for (auto& tab : state.active_tab()) { - (void) state.popup_pane(session, tab, popup_layout, di::move(create_pane_args), - context.render_thread); + if (auto session = state.active_session()) { + if (auto tab = state.active_tab()) { + (void) state.popup_pane(session.value(), tab.value(), popup_layout, + di::move(create_pane_args), context.render_thread); } } });