Created
January 7, 2023 12:10
-
-
Save wolfv/4f567fe9a17e449a6305b5ea50afa542 to your computer and use it in GitHub Desktop.
Immediate mode rendering progress bars
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <chrono> | |
#include <iostream> | |
#include <string> | |
#include <thread> | |
#include <random> | |
#include <sys/ioctl.h> | |
namespace cursor | |
{ | |
class CursorMovementTriple | |
{ | |
public: | |
CursorMovementTriple(const char* esc, int n, const char* mod) | |
: m_esc(esc) | |
, m_mod(mod) | |
, m_n(n) | |
{ | |
} | |
const char* m_esc; | |
const char* m_mod; | |
int m_n; | |
}; | |
inline std::ostream& operator<<(std::ostream& o, const CursorMovementTriple& m) | |
{ | |
o << m.m_esc << m.m_n << m.m_mod; | |
return o; | |
} | |
class CursorMod | |
{ | |
public: | |
CursorMod(const char* mod) | |
: m_mod(mod) | |
{ | |
} | |
std::ostream& operator<<(std::ostream& o) | |
{ | |
o << m_mod; | |
return o; | |
} | |
const char* m_mod; | |
}; | |
inline std::ostream& operator<<(std::ostream& o, const CursorMod& m) | |
{ | |
o << m.m_mod; | |
return o; | |
} | |
inline auto up(int n) | |
{ | |
return CursorMovementTriple("\x1b[", n, "A"); | |
} | |
inline auto down(int n) | |
{ | |
return CursorMovementTriple("\x1b[", n, "B"); | |
} | |
inline auto forward(int n) | |
{ | |
return CursorMovementTriple("\x1b[", n, "C"); | |
} | |
inline auto back(int n) | |
{ | |
return CursorMovementTriple("\x1b[", n, "D"); | |
} | |
inline auto next_line(int n) | |
{ | |
return CursorMovementTriple("\x1b[", n, "E"); | |
} | |
inline auto prev_line(int n) | |
{ | |
return CursorMovementTriple("\x1b[", n, "F"); | |
} | |
inline auto horizontal_abs(int n) | |
{ | |
return CursorMovementTriple("\x1b[", n, "G"); | |
} | |
inline auto home() | |
{ | |
return CursorMod("\x1b[H"); | |
} | |
inline auto erase_display(int n = 0) | |
{ | |
return CursorMovementTriple("\x1b[", n, "J"); | |
} | |
inline auto erase_line(int n = 0) | |
{ | |
return CursorMovementTriple("\x1b[", n, "K"); | |
} | |
inline auto scroll_up(int n = 1) | |
{ | |
return CursorMovementTriple("\x1b[", n, "S"); | |
} | |
inline auto scroll_down(int n = 1) | |
{ | |
return CursorMovementTriple("\x1b[", n, "T"); | |
} | |
inline auto show() | |
{ | |
return CursorMod("\x1b[?25h"); | |
} | |
inline auto hide() | |
{ | |
return CursorMod("\x1b[?25l"); | |
} | |
inline auto pos() | |
{ | |
return CursorMod("\x1b[R"); | |
} | |
inline auto delete_line(int n = 1) | |
{ | |
return CursorMovementTriple("\x1b[", n, "M"); | |
} | |
inline auto alternate_screen() | |
{ | |
return CursorMod("\x1b[?1049h"); | |
} | |
inline auto main_screen() | |
{ | |
return CursorMod("\x1b[?1049l"); | |
} | |
} // namespace cursor | |
struct progress_bar_state { | |
size_t total; | |
size_t current; | |
std::chrono::time_point<std::chrono::system_clock> start; | |
std::string suffix; | |
std::string prefix; | |
bool draw_speed; | |
bool draw_time; | |
bool is_bytes; | |
std::mutex* mtx = nullptr; | |
void set_state(progress_bar_state state) { | |
if (mtx) | |
mtx->lock(); | |
total = state.total; | |
current = state.current; | |
start = state.start; | |
suffix = state.suffix; | |
prefix = state.prefix; | |
draw_speed = state.draw_speed; | |
draw_time = state.draw_time; | |
is_bytes = state.is_bytes; | |
if (mtx) | |
mtx->unlock(); | |
} | |
}; | |
int get_console_width() | |
{ | |
#ifndef _WIN32 | |
struct winsize w; | |
ioctl(0, TIOCGWINSZ, &w); | |
return w.ws_col; | |
#else | |
CONSOLE_SCREEN_BUFFER_INFO coninfo; | |
auto res = GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo); | |
return coninfo.dwSize.X; | |
#endif | |
} | |
void progress_bar_render(const progress_bar_state& state) { | |
// render progress bar | |
auto terminal_width = get_console_width(); | |
std::string suffix = state.suffix; | |
if (state.draw_speed) { | |
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - state.start).count(); | |
auto speed = (double)state.current / (double)elapsed; | |
if (state.is_bytes) { | |
if (speed > 1024 * 1024) { | |
suffix += " " + std::to_string(speed / (1024 * 1024)) + " MB/s"; | |
} else if (speed > 1024) { | |
suffix += " " + std::to_string(speed / 1024) + " KB/s"; | |
} else { | |
suffix += " " + std::to_string(speed) + " B/s"; | |
} | |
} | |
} | |
auto bar_width = terminal_width - 4 - suffix.size() - state.prefix.size(); | |
auto progress = (double)state.current / (double)state.total; | |
auto pos = (size_t)(bar_width * progress); | |
std::cout << state.prefix << " ["; | |
for (size_t i = 0; i < bar_width; ++i) { | |
if (i < pos) std::cout << "="; | |
else if (i == pos) std::cout << ">"; | |
else std::cout << " "; | |
} | |
std::cout << "] " << suffix << "\n" << std::flush; | |
} | |
class progress_bar_manager { | |
public: | |
void start() { | |
running = true; | |
render_thread = std::thread(&progress_bar_manager::render_loop, this); | |
} | |
void add_progress_bar(std::shared_ptr<progress_bar_state> state) { | |
std::lock_guard<std::mutex> lock(mutex); | |
state->mtx = &mutex; | |
states.push_back(state); | |
} | |
void render_loop() { | |
int i = 0; | |
while (running) { | |
std::this_thread::sleep_for(std::chrono::milliseconds(100)); | |
std::lock_guard<std::mutex> lock(mutex); | |
if (i) | |
std::cout << cursor::up(states.size()); | |
for (auto& state : states) { | |
progress_bar_render(*state); | |
} | |
i = 1; | |
} | |
} | |
~progress_bar_manager() { | |
running = false; | |
render_thread.join(); | |
} | |
private: | |
bool running = false; | |
std::mutex mutex; | |
std::thread render_thread; | |
std::vector<std::shared_ptr<progress_bar_state>> states; | |
}; | |
std::shared_ptr<progress_bar_state> create_initial_state(size_t total, std::string name) { | |
auto state = std::make_shared<progress_bar_state>(); | |
state->total = total; | |
state->current = 0; | |
state->start = std::chrono::system_clock::now(); | |
state->suffix = "0%"; | |
state->prefix = "Progress " + name; | |
state->draw_speed = true; | |
state->draw_time = true; | |
state->is_bytes = true; | |
return state; | |
} | |
static auto dev = std::random_device(); | |
progress_bar_state get_new_state(const progress_bar_state& old) { | |
progress_bar_state new_state = old; | |
new_state.current += dev() % 10000; | |
new_state.suffix = std::to_string((size_t)(100.0 * (double)new_state.current / (double)new_state.total)) + "%"; | |
return new_state; | |
} | |
int main() { | |
auto s1 = create_initial_state(100000, "1"); | |
auto s2 = create_initial_state(100000, "2"); | |
auto s3 = create_initial_state(100000, "3"); | |
progress_bar_manager manager; | |
manager.add_progress_bar(s1); | |
manager.add_progress_bar(s2); | |
manager.add_progress_bar(s3); | |
manager.start(); | |
for (size_t i = 0; i < 100; ++i) { | |
s1->set_state(get_new_state(*s1)); | |
s2->set_state(get_new_state(*s2)); | |
s3->set_state(get_new_state(*s3)); | |
std::this_thread::sleep_for(std::chrono::milliseconds(dev() % 100)); | |
} | |
// std::cout << std::endl; | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment