Init
This commit is contained in:
commit
b91098d3d4
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
81
.gitignore
vendored
Normal file
81
.gitignore
vendored
Normal 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
8
.idea/.gitignore
vendored
Normal 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
11
.idea/borderline.iml
Normal 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
8
.idea/modules.xml
Normal 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
6
.idea/vcs.xml
Normal 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
14
Cargo.toml
Normal 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
19
LICENSE
Normal 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
53
README.md
Normal 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
BIN
images/done.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
images/progressbar.png
(Stored with Git LFS)
Normal file
BIN
images/progressbar.png
(Stored with Git LFS)
Normal file
Binary file not shown.
261
src/lib.rs
Normal file
261
src/lib.rs
Normal 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));
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user