Skip to content

Commit e544bc4

Browse files
authored
Implement terminal_pager for log subcommand (#46)
* Implement terminal_pager for log subcommand * Include cstdint * Use stringbuf instead of ostringstream * Remove m_grabbed * Pass by value to process_input * Separate namespace for ANSI code sequences to avoid magic strings * alternative_buffer scope object
1 parent bd07ea7 commit e544bc4

File tree

8 files changed

+388
-2
lines changed

8 files changed

+388
-2
lines changed

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,16 @@ set(GIT2CPP_SRC
5858
${GIT2CPP_SOURCE_DIR}/subcommand/reset_subcommand.hpp
5959
${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.cpp
6060
${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.hpp
61+
${GIT2CPP_SOURCE_DIR}/utils/ansi_code.cpp
62+
${GIT2CPP_SOURCE_DIR}/utils/ansi_code.hpp
6163
${GIT2CPP_SOURCE_DIR}/utils/common.cpp
6264
${GIT2CPP_SOURCE_DIR}/utils/common.hpp
6365
${GIT2CPP_SOURCE_DIR}/utils/git_exception.cpp
6466
${GIT2CPP_SOURCE_DIR}/utils/git_exception.hpp
67+
${GIT2CPP_SOURCE_DIR}/utils/output.cpp
6568
${GIT2CPP_SOURCE_DIR}/utils/output.hpp
69+
${GIT2CPP_SOURCE_DIR}/utils/terminal_pager.cpp
70+
${GIT2CPP_SOURCE_DIR}/utils/terminal_pager.hpp
6671
${GIT2CPP_SOURCE_DIR}/wrapper/annotated_commit_wrapper.cpp
6772
${GIT2CPP_SOURCE_DIR}/wrapper/annotated_commit_wrapper.hpp
6873
${GIT2CPP_SOURCE_DIR}/wrapper/branch_wrapper.cpp

src/subcommand/log_subcommand.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <termcolor/termcolor.hpp>
88

99
#include "log_subcommand.hpp"
10+
#include "../utils/terminal_pager.hpp"
1011
#include "../wrapper/repository_wrapper.hpp"
1112
#include "../wrapper/commit_wrapper.hpp"
1213

@@ -90,6 +91,8 @@ void log_subcommand::run()
9091
git_revwalk_new(&walker, repo);
9192
git_revwalk_push_head(walker);
9293

94+
terminal_pager pager;
95+
9396
std::size_t i=0;
9497
git_oid commit_oid;
9598
while (!git_revwalk_next(&commit_oid, walker) && i<m_max_count_flag)
@@ -100,4 +103,6 @@ void log_subcommand::run()
100103
}
101104

102105
git_revwalk_free(walker);
106+
107+
pager.show();
103108
}

src/utils/ansi_code.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#include "ansi_code.hpp"
2+
3+
namespace ansi_code
4+
{
5+
std::string cursor_to_row(size_t row)
6+
{
7+
return "\e[" + std::to_string(row) + "H";
8+
}
9+
10+
bool is_down_arrow(std::string str)
11+
{
12+
return str == "\e[B" || str == "\e[1B]";
13+
}
14+
15+
bool is_escape_char(char ch)
16+
{
17+
return ch == '\e';
18+
}
19+
20+
bool is_up_arrow(std::string str)
21+
{
22+
return str == "\e[A" || str == "\e[1A]";
23+
}
24+
}

src/utils/ansi_code.hpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#pragma once
2+
3+
#include <string>
4+
5+
/**
6+
* ANSI escape codes.
7+
* Use `termcolor` for colours.
8+
*/
9+
namespace ansi_code
10+
{
11+
// Constants.
12+
const std::string bel = "\a"; // ASCII 7, used for audio/visual feedback.
13+
const std::string cursor_to_top = "\e[H";
14+
const std::string erase_screen = "\e[2J";
15+
16+
const std::string enable_alternative_buffer = "\e[?1049h";
17+
const std::string disable_alternative_buffer = "\e[?1049l";
18+
19+
const std::string hide_cursor = "\e[?25l";
20+
const std::string show_cursor = "\e[?25h";
21+
22+
// Functions.
23+
std::string cursor_to_row(size_t row);
24+
25+
bool is_escape_char(char ch);
26+
27+
bool is_down_arrow(std::string str);
28+
bool is_up_arrow(std::string str);
29+
}

src/utils/output.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#include "output.hpp"
2+
3+
// OS-specific libraries.
4+
#include <sys/ioctl.h>
5+
6+
alternative_buffer::alternative_buffer()
7+
{
8+
tcgetattr(fileno(stdin), &m_previous_termios);
9+
auto new_termios = m_previous_termios;
10+
// Disable canonical mode (buffered I/O) and echo from stdin to stdout.
11+
new_termios.c_lflag &= (~ICANON & ~ECHO);
12+
tcsetattr(fileno(stdin), TCSANOW, &new_termios);
13+
14+
std::cout << ansi_code::enable_alternative_buffer;
15+
}
16+
17+
alternative_buffer::~alternative_buffer()
18+
{
19+
std::cout << ansi_code::disable_alternative_buffer;
20+
21+
// Restore previous termios settings.
22+
tcsetattr(fileno(stdin), TCSANOW, &m_previous_termios);
23+
}

src/utils/output.hpp

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
#pragma once
22

33
#include <iostream>
4+
#include "ansi_code.hpp"
45
#include "common.hpp"
56

7+
// OS-specific libraries.
8+
#include <termios.h>
9+
610
// Scope object to hide the cursor. This avoids
711
// cursor twinkling when rewritting the same line
812
// too frequently.
913
struct cursor_hider : noncopyable_nonmovable
1014
{
1115
cursor_hider()
1216
{
13-
std::cout << "\e[?25l";
17+
std::cout << ansi_code::hide_cursor;
1418
}
1519

1620
~cursor_hider()
1721
{
18-
std::cout << "\e[?25h";
22+
std::cout << ansi_code::show_cursor;
1923
}
2024
};
25+
26+
// Scope object to use alternative output buffer for
27+
// fullscreen interactive terminal input/output.
28+
class alternative_buffer : noncopyable_nonmovable
29+
{
30+
public:
31+
alternative_buffer();
32+
33+
~alternative_buffer();
34+
35+
private:
36+
struct termios m_previous_termios;
37+
};

src/utils/terminal_pager.cpp

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#include <cctype>
2+
#include <cstdint>
3+
#include <cstdio>
4+
#include <iostream>
5+
#include <ranges>
6+
7+
// OS-specific libraries.
8+
#include <sys/ioctl.h>
9+
10+
#include <termcolor/termcolor.hpp>
11+
12+
#include "ansi_code.hpp"
13+
#include "output.hpp"
14+
#include "terminal_pager.hpp"
15+
16+
terminal_pager::terminal_pager()
17+
: m_rows(0), m_columns(0), m_start_row_index(0)
18+
{
19+
maybe_grab_cout();
20+
}
21+
22+
terminal_pager::~terminal_pager()
23+
{
24+
release_cout();
25+
}
26+
27+
std::string terminal_pager::get_input() const
28+
{
29+
// Blocks until input received.
30+
std::string str;
31+
char ch;
32+
std::cin.get(ch);
33+
str += ch;
34+
35+
if (ansi_code::is_escape_char(ch)) // Start of ANSI escape sequence.
36+
{
37+
do
38+
{
39+
std::cin.get(ch);
40+
str += ch;
41+
} while (!std::isalpha(ch)); // ANSI escape sequence ends with a letter.
42+
}
43+
44+
return str;
45+
}
46+
47+
void terminal_pager::maybe_grab_cout()
48+
{
49+
// Unfortunately need to access _internal namespace of termcolor to check if a tty.
50+
if (termcolor::_internal::is_atty(std::cout))
51+
{
52+
// Should we do anything with cerr?
53+
m_cout_rdbuf = std::cout.rdbuf(&m_stringbuf);
54+
}
55+
else
56+
{
57+
m_cout_rdbuf = std::cout.rdbuf();
58+
}
59+
}
60+
61+
bool terminal_pager::process_input(std::string input)
62+
{
63+
if (input.size() == 0)
64+
{
65+
return true;
66+
}
67+
68+
switch (input[0])
69+
{
70+
case 'q':
71+
case 'Q':
72+
return true; // Exit pager.
73+
case 'u':
74+
case 'U':
75+
scroll(true, true); // Up a page.
76+
return false;
77+
case 'd':
78+
case 'D':
79+
case ' ':
80+
scroll(false, true); // Down a page.
81+
return false;
82+
case '\n':
83+
scroll(false, false); // Down a line.
84+
return false;
85+
case '\e': // ANSI escape sequence.
86+
// Cannot switch on a std::string.
87+
if (ansi_code::is_up_arrow(input))
88+
{
89+
scroll(true, false); // Up a line.
90+
return false;
91+
}
92+
else if (ansi_code::is_down_arrow(input))
93+
{
94+
scroll(false, false); // Down a line.
95+
return false;
96+
}
97+
}
98+
99+
std::cout << ansi_code::bel;
100+
return false;
101+
}
102+
103+
void terminal_pager::release_cout()
104+
{
105+
std::cout.rdbuf(m_cout_rdbuf);
106+
}
107+
108+
void terminal_pager::render_terminal() const
109+
{
110+
auto end_row_index = m_start_row_index + m_rows - 1;
111+
112+
std::cout << ansi_code::erase_screen;
113+
std::cout << ansi_code::cursor_to_top;
114+
115+
for (size_t i = m_start_row_index; i < end_row_index; i++)
116+
{
117+
if (i >= m_lines.size())
118+
{
119+
break;
120+
}
121+
std::cout << m_lines[i] << std::endl;
122+
}
123+
124+
std::cout << ansi_code::cursor_to_row(m_rows); // Move cursor to bottom row of terminal.
125+
std::cout << ":";
126+
}
127+
128+
void terminal_pager::scroll(bool up, bool page)
129+
{
130+
update_terminal_size();
131+
const auto old_start_row_index = m_start_row_index;
132+
size_t offset = page ? m_rows - 1 : 1;
133+
134+
if (up)
135+
{
136+
// Care needed to avoid underflow of unsigned size_t.
137+
if (m_start_row_index >= offset)
138+
{
139+
m_start_row_index -= offset;
140+
}
141+
else
142+
{
143+
m_start_row_index = 0;
144+
}
145+
}
146+
else
147+
{
148+
m_start_row_index += offset;
149+
auto end_row_index = m_start_row_index + m_rows - 1;
150+
if (end_row_index > m_lines.size())
151+
{
152+
m_start_row_index = m_lines.size() - (m_rows - 1);
153+
}
154+
}
155+
156+
if (m_start_row_index == old_start_row_index)
157+
{
158+
std::cout << ansi_code::bel;
159+
}
160+
else
161+
{
162+
render_terminal();
163+
}
164+
}
165+
166+
void terminal_pager::show()
167+
{
168+
release_cout();
169+
170+
split_input_at_newlines(m_stringbuf.view());
171+
172+
update_terminal_size();
173+
if (m_rows == 0 || m_lines.size() <= m_rows - 1)
174+
{
175+
// Don't need to use pager, can display directly.
176+
for (auto line : m_lines)
177+
{
178+
std::cout << line << std::endl;
179+
}
180+
m_lines.clear();
181+
return;
182+
}
183+
184+
alternative_buffer alt_buffer;
185+
186+
m_start_row_index = 0;
187+
render_terminal();
188+
189+
bool stop = false;
190+
do
191+
{
192+
stop = process_input(get_input());
193+
} while (!stop);
194+
195+
m_lines.clear();
196+
m_start_row_index = 0;
197+
}
198+
199+
void terminal_pager::split_input_at_newlines(std::string_view str)
200+
{
201+
auto split = str | std::ranges::views::split('\n')
202+
| std::ranges::views::transform([](auto&& range) {
203+
return std::string(range.begin(), std::ranges::distance(range));
204+
});
205+
m_lines = std::vector<std::string>{split.begin(), split.end()};
206+
}
207+
208+
void terminal_pager::update_terminal_size()
209+
{
210+
struct winsize size;
211+
int err = ioctl(fileno(stdout), TIOCGWINSZ, &size);
212+
if (err == 0)
213+
{
214+
m_rows = size.ws_row;
215+
m_columns = size.ws_col;
216+
}
217+
else
218+
{
219+
m_rows = m_columns = 0;
220+
}
221+
}

0 commit comments

Comments
 (0)