This commit is contained in:
Thorsten Sommer 2023-11-08 19:04:59 +01:00
commit b91098d3d4
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
12 changed files with 468 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.png filter=lfs diff=lfs merge=lfs -text

81
.gitignore vendored Normal file
View File

@ -0,0 +1,81 @@
/target
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# mpeltonen/sbt-idea plugin
.idea_modules/
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Rust template
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Added by cargo
#
# already existing elements were commented out
#/target
/Cargo.lock

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

11
.idea/borderline.iml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/borderline.iml" filepath="$PROJECT_DIR$/.idea/borderline.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

14
Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "borderline"
version = "1.0.0"
edition = "2021"
authors = ["Thorsten Sommer"]
description = "A simple crate to use borders in your terminal."
license = "MIT"
homepage = "https://devops.tsommer.org/open-source/rust/crates/borderline"
repository = "https://devops.tsommer.org/open-source/rust/crates/borderline"
readme = "README.md"
keywords = ["border", "terminal", "cli", "progressbar"]
categories = ["command-line-interface"]
[dependencies]

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2023 Thorsten Sommer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

53
README.md Normal file
View File

@ -0,0 +1,53 @@
# Borderline
Borderline is a Crate to render borders to the terminal. You can place text or progress bars inside the borders. The usage is as simple as:
```rust
use borderline::LineBuilder;
// Create a line builder:
let mut lb = LineBuilder::new();
// Print the header first:
borderline::print_header("Borderline Test-Drive");
// Now, we are able to attach as many "line" as we want.
// Every line must be started with a call to `line_begin`
// and ended with a call to `line_end`:
lb.line_begin("- Loading...");
// perform other actions here...
// When ending a line, you can specify the second part
// of the line, which will be printed on the right side:
lb.line_end("done.");
// You can also print a progress bar:
lb.line_begin("- Loading many files:");
// Set up the progress bar using the total number of steps:
lb.progress_bar_setup(1_000);
// Do some work and update the progress bar:
for i in 0..1_000 {
sleep(std::time::Duration::from_millis(50));
lb.progress_bar_update(i);
}
// End the progress bar:
lb.progressbar_end();
// End the line and print the second text part:
lb.line_end("done.");
// At the end, we can print the closing border:
borderline::print_border();
```
While the progress bar is active:
![progressbar.png](images/progressbar.png)
When the program is done:
![done.png](images/done.png)
In case that the given text is too long, it gets wrapped at word boundaries. When this is not possible, the text is wrapped at the given width.

BIN
images/done.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
images/progressbar.png (Stored with Git LFS) Normal file

Binary file not shown.

261
src/lib.rs Normal file
View File

@ -0,0 +1,261 @@
use std::io;
use std::io::Write;
// How big should the fence be?
const LEN: usize = 66;
// How much should the text be indented? Applies to lines, not the header.
const INNER_INDENT: usize = 1;
/// Prints a header.
pub fn print_header(text: &str) {
/// Left-aligns a string with spaces.
fn left_aligned_string(s: &str, total_len: usize) -> String {
let text_len = s.chars().count();
if text_len >= total_len {
return s.to_string();
}
let padding = total_len - text_len;
format!("{}{}", s, " ".repeat(padding))
}
// Top border:
print_border();
//
// Text processing:
//
let mut remaining = text.to_string();
while !remaining.is_empty() {
// Fits the remaining text on one line?
if remaining.chars().count() <= LEN - 4 {
// Yes, print it:
println!("| {} |", left_aligned_string(&remaining, LEN - 4));
break;
} else {
// No, we need to split the text into multiple lines.
// We want to split at word boundaries, though.
let mut line = String::from(&remaining[..LEN-4]);
if let Some(last_space) = line.rfind(' ') {
line.truncate(last_space);
remaining = remaining[last_space..].trim_start().to_string();
} else {
line = String::from(&remaining[..LEN-4]);
remaining = remaining[LEN-4..].trim_start().to_string();
}
println!("| {} |", left_aligned_string(&line, LEN - 4));
}
}
// Bottom border:
print_border();
}
/// A helper struct to build lines according to the fence.
pub struct LineBuilder {
/// The current line.
current_line: String,
/// The current line prompt, when rendering a progress bar.
current_line_prompt: String,
/// The total number of steps in the progress bar.
pb_num_steps: usize,
/// The number of characters available for the progress bar.
pb_chars_available: usize,
/// The number of steps per character in the progress bar.
pb_steps_per_char: f64,
}
/// Default implementation for LineBuilder.
impl Default for LineBuilder {
fn default() -> Self {
Self::new()
}
}
/// Implementation for LineBuilder.
impl LineBuilder {
/// Creates a new LineBuilder.
#[allow(clippy::repeat_once)]
pub fn new() -> Self {
Self {
current_line: " ".repeat(INNER_INDENT),
current_line_prompt: " ".repeat(INNER_INDENT),
pb_num_steps: 0,
pb_steps_per_char: 0.0,
pb_chars_available: 0,
}
}
/// Starts a new line. When showing a progress bar, the text will be used as a prompt.
#[allow(clippy::repeat_once)]
pub fn line_begin(&mut self, text: &str) {
// Reset the state:
self.current_line.clear();
self.current_line_prompt.clear();
let inner_indent = " ".repeat(INNER_INDENT);
self.current_line.push_str(&inner_indent);
self.current_line_prompt.push_str(&inner_indent);
self.current_line.push_str(text);
self.current_line_prompt.push_str(text);
self.print_and_clear(false);
io::stdout().flush().unwrap();
}
/// Ends the current line. After calling this, a progress bar cannot be shown anymore.
pub fn line_end(&mut self, text: &str) {
print!("\r"); // Go back to the beginning of the line
self.current_line.push(' ');
self.current_line.push_str(text);
self.print_and_clear(true);
}
/// Starts a progress bar with the given number of total steps. You must start a new line
/// before calling this.
pub fn progress_bar_setup(&mut self, num_steps: usize) {
print!("\r"); // Go back to the beginning of the line
// Determine how many characters we have left for the progress bar:
self.pb_chars_available = LEN - 4 - 4 - self.current_line_prompt.chars().count();
self.pb_num_steps = num_steps;
// Determine how many steps we can fit into one character:
if self.pb_chars_available >= self.pb_num_steps {
self.pb_steps_per_char = self.pb_chars_available as f64 / num_steps as f64;
} else {
self.pb_steps_per_char = num_steps as f64 / self.pb_chars_available as f64;
}
// Print the prompt:
self.current_line.clear();
self.current_line.push_str(self.current_line_prompt.as_str());
// Print the progress bar:
self.current_line.push(' ');
self.current_line.push('>');
self.current_line.push_str(&".".repeat(self.pb_chars_available));
self.print_and_clear(false);
io::stdout().flush().unwrap();
}
/// Updates the progress bar with the given current step.
pub fn progress_bar_update(&mut self, current_step: usize) {
// Ensure we don't go over the number of steps:
let current_step = if current_step > self.pb_num_steps {
self.pb_num_steps
} else {
current_step
};
// Determine how many characters we need to use to show the passed steps:
let num_filled = if self.pb_chars_available >= self.pb_num_steps {
self.pb_steps_per_char.floor() as usize * current_step
} else {
current_step / self.pb_steps_per_char as usize
};
// Ensure we don't go over the number of available characters:
let len_bar = if num_filled >= self.pb_chars_available {
self.pb_chars_available - 1
} else {
num_filled
};
// Build the progress bar:
print!("\r"); // Go back to the beginning of the line
self.current_line.clear();
// Print the prompt:
self.current_line.push_str(self.current_line_prompt.as_str());
// Print the progress bar:
self.current_line.push(' ');
self.current_line.push_str(&"#".repeat(len_bar));
self.current_line.push('>');
// Print the remaining characters for the future progress area:
if len_bar < self.pb_chars_available {
self.current_line.push_str(&".".repeat(self.pb_chars_available - len_bar));
}
// Finish the line:
self.current_line.push(' ');
self.print_and_clear(false);
io::stdout().flush().unwrap();
}
/// Ends the progress bar. After this call, you cannot update the progress bar anymore.
/// You must end the current line, though.
pub fn progressbar_end(&mut self) {
print!("\r"); // Go back to the beginning of the line
self.current_line.clear();
self.current_line.push_str(self.current_line_prompt.as_str());
self.print_and_clear(false);
io::stdout().flush().unwrap();
}
/// Prints the current line and clears it.
fn print_and_clear(&mut self, finalize: bool) {
let mut line_break_occurred = false; // Track if we had a line break
while !self.current_line.is_empty() {
if self.current_line.chars().count() <= LEN - 4 {
if finalize {
println!("| {} |", format_line(&self.current_line, LEN - 4));
self.current_line.clear();
self.current_line_prompt.clear();
} else {
print!("| {} |", format_line(&self.current_line, LEN - 4));
}
break; // Break when not finalizing
} else {
let mut line = String::from(&self.current_line[..LEN-4]);
let original_line = self.current_line.clone();
if let Some(last_space) = line.rfind(' ') {
line.truncate(last_space);
self.current_line = format!("{}{}", " ".repeat(INNER_INDENT), &self.current_line[last_space..].trim_start());
} else {
line = String::from(&self.current_line[..LEN-4]);
self.current_line = format!("{}{}", " ".repeat(INNER_INDENT), &self.current_line[LEN-4..].trim_start());
}
if self.current_line == original_line {
self.current_line.clear(); // Prevent infinite loop
}
println!("| {} |", format_line(&line, LEN - 4));
line_break_occurred = true; // Set flag since we had a line break
}
}
if line_break_occurred {
println!("| {} |", " ".repeat(LEN - 4)); // Print an empty line if we had a line break
}
}
}
/// Formats a line with spaces.
fn format_line(s: &str, total_len: usize) -> String {
let text_len = s.chars().count();
format!("{}{}", s, " ".repeat(total_len - text_len))
}
/// Prints a border.
pub fn print_border() {
println!("+{}+", "-".repeat(LEN - 2));
}