LOTS OF PROGRESS!

This commit is contained in:
2026-04-20 19:26:24 -06:00
parent 2389da9759
commit e8881f3a76
9 changed files with 733 additions and 52 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/out
/bin

View File

@@ -4,5 +4,7 @@
"path": "."
}
],
"settings": {}
"settings": {
"terminal.integrated.defaultProfile.windows": "MSYS2 UCRT",
}
}

2
out.txt Normal file
View File

@@ -0,0 +1,2 @@
g++ -std=c++20 -O2 -Wall -Werror -Wextra -Wshadow -Wnon-virtual-dtor -Wold-style-cast -Wcast-align -Wunused -Woverloaded-virtual -Wconversion -Wsign-conversion -Wnull-dereference -Wdouble-promotion -Wformat=2 -Wimplicit-fallthrough -Wsuggest-override -Wextra-semi -Wduplicated-cond -Wduplicated-branches -Wlogical-op -Wuseless-cast -I./src/ -c -o bin/desktoplib/terminal/Terminal.o src/desktoplib/terminal/Terminal.cpp
g++ -std=c++20 -static-libstdc++ -static-libgcc -Wl,--fatal-warnings -Wl,--warn-common -o out/out.exe bin/desktoplib/main.o bin/desktoplib/os/unix.o bin/desktoplib/os/windows.o bin/desktoplib/terminal/Terminal.o

103
src/desktoplib/main.cpp Normal file
View File

@@ -0,0 +1,103 @@
#include <desktoplib/terminal/Terminal.hpp>
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
int main() {
using namespace ckitty::terminal;
Terminal term;
// 1. Setup the screen
term.altbuff(true) // Use alternative buffer (clean exit)
.cursor(false) // Hide cursor for cleaner UI
.cls() // Clear screen
.fill("\033[48;2;20;20;20m"); // Fill background with dark grey (RGB)
// 2. Draw a Header using subprint and center
term << pos{1, 1} << backg::BLUE << color::WHITE << style::BOLD;
term.center(term.getSize().x, " TERMINAL LIBRARY TEST SUITE ");
term << style::RESET;
// 3. Demonstrate Cartesian Positioning and Colors
pos center = { term.getSize().x / 2, term.getSize().y / 2 };
term << pos{center.x - 10, 5} << color::GREEN << "Green Text at (x-10, 5)"
<< pos{center.x - 10, 6} << color::B_RED << style::ITALIC << "Bright Red Italic"
<< style::RESET;
// 4. Test RGB and HSL colors
term << pos{2, 10} << "RGB Test: ";
for (int i = 0; i < 5; ++i) {
term << color::rgb(50 * i, 255 - (50 * i), 150) << "";
}
term << pos{2, 11} << "HSL Test: ";
for (float h = 0.0f; h < 1.0f; h += 0.2f) {
term << color::hsl(h, 0.8f, 0.5f) << "";
}
term << style::RESET;
// 5. Test subprint_center
std::string long_msg = "This is a very long string that we will slice and center.";
term << pos{1, 15} << color::YELLOW;
// Centers only the word "long string" in a width of 40
term.subprint_center(10, 21, long_msg);
term << style::RESET;
// 6. Interaction Loop
term << pos{2, term.getSize().y - 2} << style::FAINT
<< "Press 'q' to quit, or any arrow key to move the cursor..." << style::RESET;
pos player = center;
bool running = true;
while (running) {
// Draw "Player"
term << player << color::CYAN << "X";
term.flush();
int k = term.getKey(); // Blocking wait
// Erase old "Player"
term << player << " ";
if (k == 'q' || k == key::ESC) {
running = false;
} else if (k == key::UP) {
player.y--;
term << color::GREEN << pos(3, 2) << "U";
} else if (k == key::DOWN) {
player.y++;
term << color::GREEN << pos(3, 2) << "D";
} else if (k == key::LEFT) {
player.x--;
term << color::GREEN << pos(3, 2) << "L";
} else if (k == key::RIGHT) {
player.x++;
term << color::GREEN << pos(3, 2) << "R";
}
term << color::RED;
term << pos(3, 3) << player.x;
term << pos(3, 4) << player.y;
// Clamp player to screen bounds
pos sz = term.getSize();
term << color::BLUE;
term << pos(3, 5) << sz.x;
term << pos(3, 6) << sz.y;
if (player.x < 1) player.x = 1;
if (player.y < 2) player.y = 2; // Keep below header
if (player.x > sz.x) player.x = sz.x;
if (player.y > sz.y) player.y = sz.y;
}
// 7. Cleanup
term.altbuff(false) // Return to normal buffer
.cursor(true) // Show cursor again
.cls();
std::cout << "Test completed successfully.\n";
return 0;
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <optional>
#include <string>
namespace ckitty {
namespace terminal {
namespace os {
int getKey(bool blocking);
std::optional<std::string> getChar(bool blocking);
std::pair<int, int> getSize();
void setRawMode(bool enable);
}
}
}

View File

@@ -0,0 +1,73 @@
#if defined(__unix__) || defined(__APPLE__) || defined(__linux__)
#include "common.hpp"
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <fcntl.h>
namespace ckitty {
namespace terminal {
namespace os {
int getKey(bool blocking) {
if (!blocking) {
// Set non-blocking read
int oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
unsigned char ch;
int n = read(STDIN_FILENO, &ch, 1);
fcntl(STDIN_FILENO, F_SETFL, oldf);
return (n > 0) ? ch : -1;
}
unsigned char buf[3];
int n = read(STDIN_FILENO, &buf, 3);
if (n == 1) {
if (buf[0] == 127) return key::BACKSPACE;
return buf[0]; // Standard ASCII
}
// Handle ANSI Escape Sequences (Arrows)
if (n == 3 && buf[0] == 0x1B && buf[1] == '[') {
switch (buf[2]) {
case 'A': return key::UP;
case 'B': return key::DOWN;
case 'C': return key::RIGHT;
case 'D': return key::LEFT;
}
}
return key::UNKNOWN;
}
std::optional<std::string> getChar(bool blocking) {
int ch = getKey(blocking);
if (ch == -1) return std::nullopt;
return std::string(1, static_cast<char>(ch));
}
std::pair<int, int> getSize() {
struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
return { w.ws_col, w.ws_row };
}
void setRawMode(bool enable) {
static struct termios oldt, newt;
if (enable) {
tcgetattr(STDIN_FILENO, &oldt);
newt = oldt;
newt.c_lflag &= ~(ICANON | ECHO); // Disable buffering and local echo
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
} else {
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
}
}
}
}
}
#endif

View File

@@ -0,0 +1,70 @@
#ifdef _WIN32
#include "common.hpp"
#include <windows.h>
#include <conio.h>
#include <desktoplib/terminal/Terminal.hpp>
namespace ckitty {
namespace terminal {
namespace os {
int getKey(bool blocking) {
// Handle non-blocking check for the console
if (!blocking && !_kbhit()) {
return -1;
}
// _getch() blocks by default
int ch = _getch();
// Check for "Extended" keys (Prefixes 0 or 224)
if (ch == 0 || ch == 224) {
int extended = _getch(); // Get the second byte
switch (extended) {
case 72: return key::UP;
case 80: return key::DOWN;
case 75: return key::LEFT;
case 77: return key::RIGHT;
default: return key::UNKNOWN;
}
}
// Handle standard keys and normalize them to your 'key' struct
switch (ch) {
case 13: return key::ENTER; // Windows uses CR (\r) for Enter
case 27: return key::ESC; // Escape
case 8: return key::BACKSPACE; // Backspace
default: return ch; // Standard ASCII (a, b, 1, 2, etc.)
}
}
std::optional<std::string> getChar(bool blocking) {
int ch = getKey(blocking);
if (ch == -1) return std::nullopt;
return std::string(1, static_cast<char>(ch));
}
std::pair<int, int> getSize() {
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
return { csbi.srWindow.Right - csbi.srWindow.Left + 1, csbi.srWindow.Bottom - csbi.srWindow.Top + 1 };
}
void setRawMode(bool enable) {
// Windows CMD/Terminal usually handles _getch automatically,
// but for Virtual Terminal sequences:
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD dwMode = 0;
GetConsoleMode(hOut, &dwMode);
if (enable) dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(hOut, dwMode);
}
}
}
}
#endif

View File

@@ -0,0 +1,346 @@
#include "Terminal.hpp"
#include <iostream>
#include <algorithm>
#include <desktoplib/os/common.hpp>
namespace ckitty {
namespace terminal {
static const auto CSI = "\x1B[";
// ------ //
pos::pos(int _x, int _y)
: x(_x), y(_y) {
}
move::move(int _x, int _y)
: x(_x), y(_y) {
}
// ------ //
// Private Constructor
style::style(const std::string& c) : code(c) {}
// Static Constants (Standard ANSI SGR codes)
const style style::RESET("\033[0m");
const style style::BOLD("\033[1m");
const style style::FAINT("\033[2m"); // Often called "dim"
const style style::ITALIC("\033[3m");
const style style::STRIKE("\033[9m");
style::operator std::string_view() const {
return std::string_view(code);
}
// ------ //
// Constructor
backg::backg(const std::string& c) : code(c) {}
// Standard Background Colors (40-47)
const backg backg::BLACK("\033[40m");
const backg backg::RED("\033[41m");
const backg backg::GREEN("\033[42m");
const backg backg::YELLOW("\033[43m");
const backg backg::BLUE("\033[44m");
const backg backg::MAGENTA("\033[45m");
const backg backg::CYAN("\033[46m");
const backg backg::WHITE("\033[47m");
// Bright Background Variants (100-107)
const backg backg::B_BLACK("\033[100m");
const backg backg::B_RED("\033[101m");
const backg backg::B_GREEN("\033[102m");
const backg backg::B_YELLOW("\033[103m");
const backg backg::B_BLUE("\033[104m");
const backg backg::B_MAGENTA("\033[105m");
const backg backg::B_CYAN("\033[106m");
const backg backg::B_WHITE("\033[107m");
// RGB Background (TrueColor: ESC[48;2;r;g;bm)
backg backg::rgb(int r, int g, int b) {
return backg("\033[48;2;" + std::to_string(r) + ";" + std::to_string(g) + ";" + std::to_string(b) + "m");
}
// HSL Background
backg backg::hsl(float h, float s, float l) {
auto hueToRgb = [](float p, float q, float t) {
if (t < 0.0f) t += 1.0f;
if (t > 1.0f) t -= 1.0f;
if (t < 1.0f / 6.0f) return p + (q - p) * 6.0f * t;
if (t < 1.0f / 2.0f) return q;
if (t < 2.0f / 3.0f) return p + (q - p) * (2.0f / 3.0f - t) * 6.0f;
return p;
};
float r, g, b;
if (s == 0.0f) {
r = g = b = l;
} else {
float q = l < 0.5f ? l * (1.0f + s) : l + s - l * s;
float p = 2.0f * l - q;
r = hueToRgb(p, q, h + 1.0f / 3.0f);
g = hueToRgb(p, q, h);
b = hueToRgb(p, q, h - 1.0f / 3.0f);
}
return rgb(static_cast<int>(r * 255), static_cast<int>(g * 255), static_cast<int>(b * 255));
}
backg::operator std::string_view() const {
return std::string_view(code);
}
// ------ //
// Constructor
color::color(const std::string& c) : code(c) {}
// Standard Colors (Foreground)
const color color::BLACK("\033[30m");
const color color::RED("\033[31m");
const color color::GREEN("\033[32m");
const color color::YELLOW("\033[33m");
const color color::BLUE("\033[34m");
const color color::MAGENTA("\033[35m");
const color color::CYAN("\033[36m");
const color color::WHITE("\033[37m");
// Bright Variants
const color color::B_BLACK("\033[90m");
const color color::B_RED("\033[91m");
const color color::B_GREEN("\033[92m");
const color color::B_YELLOW("\033[93m");
const color color::B_BLUE("\033[94m");
const color color::B_MAGENTA("\033[95m");
const color color::B_CYAN("\033[96m");
const color color::B_WHITE("\033[97m");
// RGB Implementation (TrueColor: ESC[38;2;r;g;bm)
color color::rgb(int r, int g, int b) {
return color("\033[38;2;" + std::to_string(r) + ";" +
std::to_string(g) + ";" +
std::to_string(b) + "m");
}
// HSL Implementation
color color::hsl(float h, float s, float l) {
// Helper to convert HSL to RGB
auto hueToRgb = [](float p, float q, float t) {
if (t < 0.0f) t += 1.0f;
if (t > 1.0f) t -= 1.0f;
if (t < 1.0f / 6.0f) return p + (q - p) * 6.0f * t;
if (t < 1.0f / 2.0f) return q;
if (t < 2.0f / 3.0f) return p + (q - p) * (2.0f / 3.0f - t) * 6.0f;
return p;
};
float r, g, b;
if (s == 0.0f) {
r = g = b = l; // Achromatic
} else {
float q = l < 0.5f ? l * (1.0f + s) : l + s - l * s;
float p = 2.0f * l - q;
r = hueToRgb(p, q, h + 1.0f / 3.0f);
g = hueToRgb(p, q, h);
b = hueToRgb(p, q, h - 1.0f / 3.0f);
}
return rgb(static_cast<int>(r * 255), static_cast<int>(g * 255), static_cast<int>(b * 255));
}
color::operator std::string_view() const {
return std::string_view(code);
}
// ------ //
Terminal::Terminal()
: _os(&std::cout), _is(&std::cin), _own(false) {
}
Terminal::~Terminal() {
if (_own) {
delete _os;
delete _is;
}
}
// ------ //
Terminal& Terminal::fill(const std::string_view color) {
// 1. Set background color
// 2. Clear screen (2J)
// 3. Move cursor to home (H)
(*_os) << color << CSI << "2J" << CSI << "H" << std::flush;
return *this;
}
Terminal& Terminal::cls() {
// Clear entire screen and move cursor to 1,1
(*_os) << CSI << "2J" << CSI << "H" << std::flush;
return *this;
}
Terminal& Terminal::altbuff(bool enable) {
// ?1049h enables alternative buffer, ?1049l disables it
(*_os) << CSI << (enable ? "?1049h" : "?1049l") << std::flush;
return *this;
}
Terminal& Terminal::createScrollRange(int start, int end) {
// DECSTBM: Set Top and Bottom Margins
(*_os) << CSI << start << ";" << end << "r" << std::flush;
return *this;
}
Terminal& Terminal::undoScrollRange() {
// Reset margins to full screen
(*_os) << CSI << "r" << std::flush;
return *this;
}
Terminal& Terminal::clearRow(int row) {
// Move to row, column 1, then clear line (2K)
(*_os) << CSI << row << ";1H" << CSI << "2K" << std::flush;
return *this;
}
Terminal& Terminal::clearRows(int start, int end) {
for (int i = start; i <= end; ++i) {
clearRow(i);
}
return *this;
}
Terminal& Terminal::cursor(bool show) {
// ?25h shows cursor, ?25l hides it
(*_os) << CSI << (show ? "?25h" : "?25l") << std::flush;
return *this;
}
// ------ //
Terminal& Terminal::wait() {
this->flush();
this->sink(); // Clear any previous keystrokes so we wait for a NEW one
this->getKey(); // Block until a key is pressed
return *this;
}
Terminal& Terminal::flush() {
_os->flush();
return *this;
}
// Updated sink() using OS-specific logic
Terminal& Terminal::sink() {
while (os::getKey(false) != -1); // Drain the buffer
return *this;
}
// ------ //
int Terminal::getKey() { return os::getKey(true); }
int Terminal::getKeyNb() { return os::getKey(false); }
std::optional<std::string> Terminal::getChar() { return os::getChar(true); }
std::optional<std::string> Terminal::getCharNb() { return os::getChar(false); }
pos Terminal::getSize() {
auto [x, y] = os::getSize();
return { x, y };
}
// ------ //
Terminal& Terminal::operator<<(const std::string_view& msg) {
(*_os) << msg;
return *this;
}
Terminal& Terminal::operator<<(const pos p) {
// ANSI uses 1-based indexing: \033[row;colH
(*_os) << CSI << p.y << ";" << p.x << "H";
return *this;
}
Terminal& Terminal::operator<<(const move m) {
if (m.y < 0) (*_os) << CSI << -m.y << "A"; // Up
else if (m.y > 0) (*_os) << CSI << m.y << "B"; // Down
if (m.x > 0) (*_os) << CSI << m.x << "C"; // Forward/Right
else if (m.x < 0) (*_os) << CSI << -m.x << "D"; // Back/Left
return *this;
}
Terminal& Terminal::operator<<(const style c) {
(*_os) << c.code;
return *this;
}
Terminal& Terminal::operator<<(const color c) {
(*_os) << c.code;
return *this;
}
Terminal& Terminal::operator<<(const backg c) {
(*_os) << c.code;
return *this;
}
// ------ //
Terminal& Terminal::center(int width, const std::string_view msg) {
if (msg.length() >= static_cast<std::size_t>(width)) {
(*_os) << msg;
} else {
std::size_t total_padding = static_cast<std::size_t>(width) - msg.length();
std::size_t left_padding = total_padding / 2;
std::size_t right_padding = total_padding - left_padding;
// Fill padding using a string constructor or a loop
// Standard strings are easiest for repeated characters
(*_os) << std::string(left_padding, ' ')
<< msg
<< std::string(right_padding, ' ');
}
return *this;
}
Terminal& Terminal::subprint(int i0, int i1, const std::string_view msg) {
// Clamp indices to valid string range
std::size_t len = msg.length();
std::size_t start = std::clamp(std::size_t(i0), std::size_t(0), len);
std::size_t end = std::clamp(std::size_t(i1), start, len);
if (start < end) {
(*_os) << msg.substr(start, end - start);
}
return *this;
}
Terminal& Terminal::subprint_center(int i0, int i1, const std::string_view msg) {
// 1. Calculate the slice from the original message
std::size_t len = msg.length();
std::size_t start = std::clamp(std::size_t(i0), std::size_t(0), len);
std::size_t end = std::clamp(std::size_t(i1), start, len);
std::string_view slice = msg.substr(start, end - start);
// 2. The desired display width is defined by the bounds i0 and i1
int target_width = i1 - i0;
// 3. Hand off to the existing center logic
return center(target_width, slice);
}
}
}

View File

@@ -4,6 +4,7 @@
#include <string>
#include <string_view>
#include <sstream>
#include <optional>
namespace ckitty {
@@ -21,7 +22,7 @@ namespace ckitty {
struct pos {
public:
int x, y;
pos(int x, int y);
pos(int _x, int _y);
};
/**
@@ -31,7 +32,7 @@ namespace ckitty {
struct move {
public:
int x, y;
move(int x, int y);
move(int _x, int _y);
};
/**
@@ -53,8 +54,11 @@ namespace ckitty {
private:
std::string_view code;
backg(const std::string_view code);
std::string code;
backg(const std::string& code);
public:
operator std::string_view() const;
public:
static backg rgb(int r, int g, int b);
@@ -79,8 +83,12 @@ namespace ckitty {
static const color WHITE; static const color B_WHITE;
private:
std::string_view code;
color(const std::string_view code);
std::string code;
color(const std::string& code);
public:
operator std::string_view() const;
public:
static color rgb(int r, int g, int b);
static color hsl(float hue, float sat, float lum);
@@ -99,8 +107,10 @@ namespace ckitty {
static const style FAINT;
static const style STRIKE;
private:
std::string_view code;
style(const std::string_view code);
std::string code;
style(const std::string& code);
public:
operator std::string_view() const;
};
/**
@@ -109,15 +119,17 @@ namespace ckitty {
struct key {
friend class Terminal;
public:
// Key Definitions (ASCII OK) //
static constexpr const char UP = 0x80;
static constexpr const char DOWN = 0x81;
static constexpr const char LEFT = 0x82;
static constexpr const char RIGHT = 0x83;
static constexpr const char ENTER = 0x84;
static constexpr const char ESC = 0x85;
static constexpr const char BACKSPACE = 0x86;
static constexpr const char UNKNOWN = 0xFF;
static constexpr int UP = 0x101;
static constexpr int DOWN = 0x102;
static constexpr int LEFT = 0x103;
static constexpr int RIGHT = 0x104;
static constexpr int ENTER = 0x0D;
static constexpr int ESC = 0x1B;
static constexpr int BACKSPACE = 0x08;
static constexpr int TAB = 0x09;
static constexpr int UNKNOWN = -1;
private:
};
@@ -147,7 +159,7 @@ namespace ckitty {
/**
* Fills the screen with a specific background color.
* @param color The background color constant (e.g., Terminal::BG_BLUE)
* @param color The background color constant (e.g., BG_BLUE)
*/
Terminal& fill(const std::string_view color);
@@ -175,19 +187,6 @@ namespace ckitty {
*/
Terminal& clearRows(int start, int end);
/**
* Clears a specific column.
* @param col The 1-based index of the column to clear.
*/
Terminal& clearColumn(int col);
/**
* Clears a range of columns (inclusive).
* @param start The first column.
* @param end The last column.
*/
Terminal& clearColumns(int start, int end);
/**
* Shows / Hides the cursor.
*/
@@ -219,6 +218,9 @@ namespace ckitty {
std::optional<std::string> getChar();
/**
* Get char non-blocking
*/
std::optional<std::string> getCharNb();
pos getSize();
@@ -237,14 +239,33 @@ namespace ckitty {
Terminal& operator<<(const move c);
Terminal& operator<<(const style c);
Terminal& operator<<(const color c);
Terminal& operator<<(const backg c);
public:
/**
* Represents an operation which will print up
* to a specific amount of characters.
* Prints a centered version of the final text,
* adding padding to center it as best as it can.
*/
template <typename T>
Terminal& subprint(int i0, int i1, const T& msg) {}
Terminal& center(int width, const std::string_view msg);
/**
* Prints a slice of the message from index i0 up to (but not including) i1.
* We convert T to a string first to handle non-string types (like int or float).
*/
Terminal& subprint(int i0, int i1, const std::string_view msg);
/**
* Combines centering logic with sub-printing.
* It takes a slice of the message, then pads it to fit the width (i1 - i0).
*/
Terminal& subprint_center(int i0, int i1, const std::string_view msg);
public:
/**
* Prints a centered version of the final text,
@@ -258,30 +279,68 @@ namespace ckitty {
std::string s = oss.str();
// then print
if (s.length() >= isize(width)) {
std::cout << s;
}
else {
isize total_padding = isize(width) - s.length();
isize left_padding = total_padding / 2;
std::cout << std::string(left_padding, ' ');
std::cout << s;
std::cout << std::string(total_padding - left_padding, ' ');
if (s.length() >= std::size_t(width)) {
(*_os) << s;
} else {
std::size_t total_padding = std::size_t(width) - s.length();
std::size_t left_padding = total_padding / 2;
(*_os) << std::string(left_padding, ' ');
(*_os) << s;
(*_os) << std::string(total_padding - left_padding, ' ');
}
return *this;
}
/**
* Centered version of the subprint version.
* Prints a slice of the message from index i0 up to (but not including) i1.
* We convert T to a string first to handle non-string types (like int or float).
*/
template <typename T>
Terminal& subprint_center(int i0, int i1, const T& msg) {}
Terminal& subprint(int i0, int i1, const T& msg) {
std::ostringstream oss;
oss << msg;
std::string s = oss.str();
// Convert to string_view for easy slicing and safety
std::string_view sv(s);
// Bounds checking to prevent crashes
std::size_t start = std::max(std::size_t(0), std::size_t(i0));
std::size_t end = std::min(std::size_t(sv.length()), std::size_t(i1));
if (start < end) {
(*_os) << sv.substr(start, end - start);
}
return *this;
}
/**
* Combines centering logic with sub-printing.
* It takes a slice of the message, then pads it to fit the width (i1 - i0).
*/
template <typename T>
Terminal& subprint_center(int i0, int i1, const T& msg) {
std::ostringstream oss;
oss << msg;
std::string s = oss.str();
std::string_view sv(s);
// Get the actual slice we want to center
std::size_t start = std::max(std::size_t(0), std::size_t(i0));
std::size_t end = std::min(std::size_t(sv.length()), std::size_t(i1));
std::string_view slice = (start < end) ? sv.substr(start, end - start) : "";
// The target width for centering is the range defined by i0 and i1
std::size_t target_width = std::size_t(i1 - i0);
return center(int(target_width), slice);
}
private:
template <typename T>
Terminal& read(T& var) {
std::cin >> var;
(*_is) >> var;
return *this;
}