Compare commits
10 Commits
f929b53ae6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
312b2f76e2 | ||
|
|
a731360ea4 | ||
|
|
882c12577d | ||
|
|
d9de028f0e | ||
|
|
8146ffee44 | ||
|
|
90a5ab038d | ||
|
|
1e6cf30fea | ||
|
|
ac66044508 | ||
|
|
3150fc3e23 | ||
|
|
1bf0555d66 |
29
.github/workflows/release.yml
vendored
Normal file
29
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: release ${{ matrix.target }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-pc-windows-gnu
|
||||
archive: zip
|
||||
- target: x86_64-unknown-linux-musl
|
||||
archive: tar.gz tar.xz tar.zst
|
||||
- target: x86_64-apple-darwin
|
||||
archive: zip
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Compile and release
|
||||
uses: rust-build/rust-build.action@v1.4.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
RUSTTARGET: ${{ matrix.target }}
|
||||
ARCHIVE_TYPES: ${{ matrix.archive }}
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -2,6 +2,17 @@
|
||||
name = "tlock"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Pihkaal <hello@pihkaal.me>"]
|
||||
|
||||
description = "Fully customizable cross-platform clock for the terminal."
|
||||
homepage = "https://github.com/pihkaal/tlock"
|
||||
repository = "https://github.com/pihkaal/tlock"
|
||||
|
||||
keywords = ["clock", "unixporn", "terminal"]
|
||||
categories = ["command-line-utilities"]
|
||||
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.31"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2024 Pihkaal <pihkaal@proton.me>
|
||||
Copyright (c) 2024 Pihkaal <hello@pihkaal.me>
|
||||
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
|
||||
@@ -19,4 +19,3 @@ 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.
|
||||
|
||||
|
||||
84
README.md
84
README.md
@@ -1,6 +1,84 @@
|
||||
# tlock
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<img src="https://i.imgur.com/EeTzHDR.png" width="200">
|
||||
<br>
|
||||
tlock
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
*⚠️ Work in progress!*
|
||||
<h4 align="center">Fully customizable terminal clock.</h4>
|
||||
|
||||
Fully customizable cross-platform clock for the terminal.
|
||||
<p align="center">
|
||||
<a href="https://www.rust-lang.org">
|
||||
<img src="https://img.shields.io/badge/rust-f54b00?style=for-the-badge&logo=rust&logoColor=white">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center" id="links">
|
||||
<a href="#description">Description</a> •
|
||||
<a href="#installation">Installation</a> •
|
||||
<a href="#how-to-use">How to use</a> •
|
||||
<a href="#configuration">Configuration</a> •
|
||||
<a href="https://pihkaal.me">Visit it</a> •
|
||||
<a href="#license">License</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
## Description
|
||||
|
||||
This is a fully customizable terminal clock written in Rust. You can change de colors, the format and you can even use multiples modes: clock, chronometer and timer.
|
||||
|
||||
<br>
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ cargo install --git https://github.com/pihkaal/tlock.git
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
# Help
|
||||
$ tlock --help
|
||||
|
||||
# Clock mode
|
||||
$tlock
|
||||
|
||||
# Debug mode (print current configuration)
|
||||
$ tlock debug
|
||||
|
||||
# Chronometer mode
|
||||
$ tlock chrono
|
||||
|
||||
# Timer mode
|
||||
$ tlock timer 4h 12m 30s
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is stored under `~/.config/tlock/config`, it is generated by the program if it doesn't exist.
|
||||
You can regenerate this configuration at any time by running:
|
||||
|
||||
```bash
|
||||
$ tlock --regenerate-default
|
||||
```
|
||||
|
||||
You can use multiple configuration files thanks to the `--config` flag:
|
||||
|
||||
```bash
|
||||
$ tlock --config /path/to/my/config
|
||||
```
|
||||
|
||||
The configuration itself contains comments to help you understand how to customize it.
|
||||
|
||||
<br>
|
||||
|
||||
## License
|
||||
|
||||
This project is <a href="https://opensource.org/licenses/MIT">MIT</a> licensed.
|
||||
|
||||
100
src/config.rs
100
src/config.rs
@@ -1,10 +1,10 @@
|
||||
use core::panic;
|
||||
use std::{fs, path::PathBuf};
|
||||
use std::{any::type_name, fs, path::PathBuf};
|
||||
|
||||
use crossterm::style::Color;
|
||||
use ini::configparser::ini::Ini;
|
||||
|
||||
use crate::{
|
||||
eprintln_quit,
|
||||
modes::debug,
|
||||
rendering::color::{generate_gradient, parse_hex_color, ComputableColor},
|
||||
};
|
||||
@@ -21,49 +21,69 @@ const DEFAULT_CONFIG: &str = include_str!("default_config");
|
||||
|
||||
pub fn load_from_file(path: PathBuf, debug_mode: bool) -> Config {
|
||||
let mut ini = Ini::new();
|
||||
ini.load(&path.to_str().unwrap()).unwrap();
|
||||
ini.load(
|
||||
&path
|
||||
.to_str()
|
||||
.unwrap_or_else(|| eprintln_quit!("Invalid configuration path")),
|
||||
)
|
||||
.unwrap_or_else(|_| eprintln_quit!("Unable to parse configuration file"));
|
||||
|
||||
let config = Config {
|
||||
be_polite: ini.getbool("general", "polite").unwrap().unwrap(),
|
||||
fps: ini.getuint("general", "fps").unwrap().unwrap(),
|
||||
Config {
|
||||
be_polite: get_ini_value(&ini, "general", "polite"),
|
||||
fps: get_ini_value(&ini, "general", "fps"),
|
||||
color: load_color(&ini, debug_mode),
|
||||
time_format: ini.get("format", "time").unwrap(),
|
||||
date_format: ini.get("format", "date").unwrap(),
|
||||
};
|
||||
|
||||
return config;
|
||||
time_format: get_ini_value(&ini, "format", "time"),
|
||||
date_format: get_ini_value(&ini, "format", "date"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_default_config(path: PathBuf) -> () {
|
||||
let parent = path.parent().unwrap();
|
||||
// Write default config file to target path
|
||||
let parent = path
|
||||
.parent()
|
||||
.unwrap_or_else(|| eprintln_quit!("Invalid configuration path"));
|
||||
let _ = fs::create_dir_all(parent);
|
||||
let _ = fs::write(path, DEFAULT_CONFIG);
|
||||
}
|
||||
|
||||
fn get_ini_value<T: std::str::FromStr>(ini: &Ini, section: &str, key: &str) -> T {
|
||||
if let Some(value) = ini.get(section, key) {
|
||||
value.parse::<T>().unwrap_or_else(|_| {
|
||||
eprintln_quit!(
|
||||
"Invalid value at {}.{}: Expected {}, got '{}'",
|
||||
section,
|
||||
key,
|
||||
type_name::<T>(),
|
||||
value
|
||||
)
|
||||
})
|
||||
} else {
|
||||
eprintln_quit!("Missing required config key: {}.{}", section, key)
|
||||
}
|
||||
}
|
||||
|
||||
fn load_color(ini: &Ini, debug_mode: bool) -> ComputableColor {
|
||||
let color_mode = ini.get("styling", "color_mode").unwrap();
|
||||
let color_mode: String = get_ini_value(&ini, "styling", "color_mode");
|
||||
|
||||
match color_mode.as_str() {
|
||||
"term" => {
|
||||
let color = ini.getint("styling", "color_term").unwrap().unwrap();
|
||||
return ComputableColor::from(load_term_color(color));
|
||||
let color: u8 = get_ini_value(&ini, "styling", "color_term");
|
||||
ComputableColor::from(load_term_color(color))
|
||||
}
|
||||
"hex" => {
|
||||
let color = ini.get("styling", "color_hex").unwrap();
|
||||
return ComputableColor::from(load_hex_color(&color));
|
||||
let color: String = get_ini_value(&ini, "styling", "color_hex");
|
||||
ComputableColor::from(load_hex_color(&color))
|
||||
}
|
||||
"ansi" => {
|
||||
let color = ini.getint("styling", "color_ansi").unwrap().unwrap();
|
||||
return ComputableColor::from(load_ansi_color(color));
|
||||
let color: u8 = get_ini_value(&ini, "styling", "color_ansi");
|
||||
ComputableColor::from(load_ansi_color(color))
|
||||
}
|
||||
"gradient" => {
|
||||
return load_gradient(ini, debug_mode);
|
||||
}
|
||||
_ => panic!("ERROR: Invalid color mode: {}", color_mode),
|
||||
"gradient" => load_gradient(ini, debug_mode),
|
||||
_ => eprintln_quit!("Invalid color mode: {}", color_mode),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_term_color(value: i64) -> Color {
|
||||
fn load_term_color(value: u8) -> Color {
|
||||
match value {
|
||||
0 => Color::Black,
|
||||
1 => Color::DarkRed,
|
||||
@@ -81,48 +101,48 @@ fn load_term_color(value: i64) -> Color {
|
||||
13 => Color::Magenta,
|
||||
14 => Color::Cyan,
|
||||
15 => Color::White,
|
||||
_ => panic!("ERROR: Invalid terminal color: {}", value),
|
||||
_ => eprintln_quit!("Invalid terminal color: {}", value),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_hex_color(value: &str) -> Color {
|
||||
let rgb = parse_hex_color(value);
|
||||
return Color::Rgb {
|
||||
Color::Rgb {
|
||||
r: rgb.0,
|
||||
g: rgb.1,
|
||||
b: rgb.2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn load_ansi_color(value: i64) -> Color {
|
||||
return Color::AnsiValue(value.try_into().unwrap());
|
||||
fn load_ansi_color(value: u8) -> Color {
|
||||
Color::AnsiValue(value)
|
||||
}
|
||||
|
||||
fn load_gradient(ini: &Ini, debug_mode: bool) -> ComputableColor {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Iterate over all gradient keys, they are defined like that in the config file:
|
||||
// gradient_key_1=...
|
||||
// gradient_key_2=...
|
||||
// gradient_key_N=...
|
||||
let mut i = 0;
|
||||
while let Some(key) = ini.get("gradient", &format!("gradient_key_{}", i)) {
|
||||
keys.push(parse_hex_color(&key));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if !debug_mode && ini.getbool("gradient", "gradient_loop").unwrap().unwrap() {
|
||||
let mut loop_keys = keys.clone();
|
||||
loop_keys.reverse();
|
||||
for i in 1..loop_keys.len() {
|
||||
keys.push(*loop_keys.get(i).unwrap());
|
||||
// Generate gradient loop if needed
|
||||
if !debug_mode && get_ini_value(&ini, "gradient", "gradient_loop") {
|
||||
for &key in keys.clone().iter().rev().skip(1) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// I use half characters for debug mode rendering, so we take display size * 2
|
||||
let steps: usize = if debug_mode {
|
||||
debug::DEBUG_COLOR_DISPLAY_SIZE * 2
|
||||
} else {
|
||||
ini.getuint("gradient", "gradient_steps")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
get_ini_value(&ini, "gradient", "gradient_steps")
|
||||
};
|
||||
return generate_gradient(keys, steps - 1);
|
||||
generate_gradient(keys, steps - 1)
|
||||
}
|
||||
|
||||
32
src/main.rs
32
src/main.rs
@@ -35,8 +35,13 @@ struct Cli {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
#[clap(alias = "d")]
|
||||
Debug {},
|
||||
|
||||
#[clap(alias = "c")]
|
||||
Chrono {},
|
||||
|
||||
#[clap(alias = "t")]
|
||||
Timer {
|
||||
#[arg(required = true)]
|
||||
duration: Vec<String>,
|
||||
@@ -46,12 +51,16 @@ enum Commands {
|
||||
fn main() -> io::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Load config
|
||||
// Load config from either given config file
|
||||
let mut default_generated = false;
|
||||
let config_file = if let Some(custom_config) = cli.config {
|
||||
PathBuf::from(custom_config)
|
||||
} else {
|
||||
let config_file = config_dir().unwrap().join("tlock").join("config");
|
||||
// Or default one, located in ~/.config/tlock
|
||||
let config_file = config_dir()
|
||||
.unwrap_or_else(|| eprintln_quit!("Unble to get configuration directory"))
|
||||
.join("tlock")
|
||||
.join("config");
|
||||
if !config_file.exists() {
|
||||
write_default_config(config_file.clone());
|
||||
default_generated = true;
|
||||
@@ -60,7 +69,10 @@ fn main() -> io::Result<()> {
|
||||
config_file
|
||||
};
|
||||
|
||||
// Regenerate default config if needed
|
||||
if cli.regenerate_default {
|
||||
// If a config file already exists and it's not the first time the config
|
||||
// is being generated, then ask for confirmation
|
||||
if !default_generated && config_file.exists() && !cli.yes {
|
||||
println!("A config file is already located at {:?}", config_file);
|
||||
print!("Do you really want to recreate it ? [y/N] ");
|
||||
@@ -77,15 +89,19 @@ fn main() -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwhise, just write default config to target path
|
||||
write_default_config(config_file.clone());
|
||||
println!("Done.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If no config file was found, throw an error
|
||||
// NOTE: this should never happen
|
||||
if !config_file.exists() {
|
||||
panic!("ERROR: Configuration file not found");
|
||||
}
|
||||
|
||||
// Enable debug mode if needed, and load config
|
||||
let debug_mode = match &cli.command {
|
||||
Some(Commands::Debug {}) => true,
|
||||
_ => false,
|
||||
@@ -93,6 +109,7 @@ fn main() -> io::Result<()> {
|
||||
let mut config = config::load_from_file(config_file, debug_mode);
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
// Print debug infos
|
||||
if debug_mode {
|
||||
debug::print_debug_infos(&mut config)?;
|
||||
return Ok(());
|
||||
@@ -102,7 +119,8 @@ fn main() -> io::Result<()> {
|
||||
execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;
|
||||
let _ = terminal::enable_raw_mode()?;
|
||||
|
||||
match &cli.command {
|
||||
// Start the appropriate mode
|
||||
let quit_reason = match &cli.command {
|
||||
Some(Commands::Chrono {}) => modes::chrono::main_loop(&mut config)?,
|
||||
Some(Commands::Timer { duration }) => {
|
||||
let duration = duration.join(" ");
|
||||
@@ -110,16 +128,16 @@ fn main() -> io::Result<()> {
|
||||
}
|
||||
Some(Commands::Debug {}) => unreachable!(),
|
||||
None => modes::clock::main_loop(&mut config)?,
|
||||
}
|
||||
};
|
||||
|
||||
// Disale raw mode, leave the alternate screen and show the cursor back
|
||||
let _ = terminal::disable_raw_mode().unwrap();
|
||||
let _ = terminal::disable_raw_mode()?;
|
||||
execute!(stdout, terminal::LeaveAlternateScreen, cursor::Show)?;
|
||||
|
||||
// Be polite
|
||||
if config.be_polite {
|
||||
println!("CTRL-C pressed, bye!\n");
|
||||
println!("{}, bye!\n", quit_reason);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ impl Chronometer {
|
||||
}
|
||||
|
||||
fn is_paused(&self) -> bool {
|
||||
return self.start_time.is_none();
|
||||
self.start_time.is_none()
|
||||
}
|
||||
|
||||
fn elapsed(&self) -> Duration {
|
||||
@@ -66,7 +66,7 @@ impl Chronometer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main_loop(config: &mut Config) -> io::Result<()> {
|
||||
pub fn main_loop(config: &mut Config) -> io::Result<String> {
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
let mut chronometer = Chronometer::new();
|
||||
@@ -75,18 +75,19 @@ pub fn main_loop(config: &mut Config) -> io::Result<()> {
|
||||
let mut lapses: Vec<Lapse> = vec![];
|
||||
let mut scroll_offset: usize = 0;
|
||||
|
||||
let mut quit = false;
|
||||
while !quit {
|
||||
let mut quit_reason = None;
|
||||
while quit_reason.is_none() {
|
||||
// Handle events
|
||||
while event::poll(Duration::ZERO)? {
|
||||
match event::read()? {
|
||||
Event::Key(e) => match e.code {
|
||||
// Handle CTRL-C
|
||||
// Handle quit via CTRL-C or Q
|
||||
KeyCode::Char('c') => {
|
||||
if e.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
quit = true;
|
||||
quit_reason = Some("CTRL-C pressed");
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => quit_reason = Some("Q pressed"),
|
||||
// Handle pause
|
||||
KeyCode::Char(' ') => {
|
||||
chronometer.toggle_pause();
|
||||
@@ -146,7 +147,7 @@ pub fn main_loop(config: &mut Config) -> io::Result<()> {
|
||||
thread::sleep(Duration::from_millis(1000 / config.fps));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
Ok(quit_reason.unwrap().to_string())
|
||||
}
|
||||
|
||||
fn render_frame(
|
||||
@@ -172,6 +173,7 @@ fn render_frame(
|
||||
*scroll_offset = lapses.len() - max_items;
|
||||
}
|
||||
|
||||
// Iterate over lapses, skipping with scroll offset and taxing N items
|
||||
for (i, lapse) in lapses
|
||||
.iter()
|
||||
.rev()
|
||||
@@ -201,5 +203,5 @@ fn render_frame(
|
||||
rendering::draw_text(text, x, y, color)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -16,21 +16,22 @@ use crate::{
|
||||
rendering::{self, symbols},
|
||||
};
|
||||
|
||||
pub fn main_loop(config: &mut Config) -> io::Result<()> {
|
||||
pub fn main_loop(config: &mut Config) -> io::Result<String> {
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
let mut quit = false;
|
||||
while !quit {
|
||||
let mut quit_reason = None;
|
||||
while quit_reason.is_none() {
|
||||
// Handle events
|
||||
while event::poll(Duration::ZERO)? {
|
||||
match event::read()? {
|
||||
Event::Key(e) => match e.code {
|
||||
// Handle CTRL-C
|
||||
// Handle quit via CTRL-C or Q
|
||||
KeyCode::Char('c') => {
|
||||
if e.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
quit = true;
|
||||
quit_reason = Some("CTRL-C pressed");
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => quit_reason = Some("Q pressed"),
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
@@ -50,7 +51,7 @@ pub fn main_loop(config: &mut Config) -> io::Result<()> {
|
||||
thread::sleep(Duration::from_millis(1000 / config.fps));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
Ok(quit_reason.unwrap().to_string())
|
||||
}
|
||||
|
||||
fn render_frame(config: &Config) -> io::Result<()> {
|
||||
@@ -73,5 +74,5 @@ fn render_frame(config: &Config) -> io::Result<()> {
|
||||
let y = height / 2 + symbols::SYMBOL_HEIGHT as i16 / 2 + 2;
|
||||
rendering::draw_text(&date, x, y - 1, color)?;
|
||||
|
||||
return Ok(());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -27,10 +27,15 @@ pub fn print_debug_infos(config: &mut Config) -> io::Result<()> {
|
||||
|
||||
print_debug_label("Color scheme")?;
|
||||
let width = config.color.get_keys_count();
|
||||
// If width is one, it is a single color
|
||||
if width == 1 {
|
||||
queue!(stdout, style::SetBackgroundColor(config.color.get_value()))?;
|
||||
write!(stdout, "{}", " ".repeat(DEBUG_COLOR_DISPLAY_SIZE))?;
|
||||
} else {
|
||||
}
|
||||
// Otherwhise, it's a gradient
|
||||
else {
|
||||
// Use half characters to display two colors in one character using background
|
||||
// and foreground
|
||||
for _ in 0..width / 2 {
|
||||
queue!(stdout, style::SetForegroundColor(config.color.get_value()))?;
|
||||
config.color.update();
|
||||
@@ -45,7 +50,8 @@ pub fn print_debug_infos(config: &mut Config) -> io::Result<()> {
|
||||
writeln!(stdout)?;
|
||||
queue!(stdout, style::ResetColor)?;
|
||||
let _ = stdout.flush();
|
||||
return Ok(());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_debug_label(key: &str) -> io::Result<()> {
|
||||
@@ -55,5 +61,5 @@ fn print_debug_label(key: &str) -> io::Result<()> {
|
||||
write!(stdout, "{}: ", key)?;
|
||||
queue!(stdout, style::ResetColor)?;
|
||||
|
||||
return Ok(());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ use crossterm::{
|
||||
terminal::{self, ClearType},
|
||||
};
|
||||
|
||||
use crate::utils;
|
||||
use crate::{
|
||||
config::Config,
|
||||
rendering::{self, symbols},
|
||||
};
|
||||
use crate::{eprintln_quit, utils};
|
||||
|
||||
struct Timer {
|
||||
duration: Duration,
|
||||
@@ -61,7 +61,7 @@ impl Timer {
|
||||
}
|
||||
|
||||
fn is_paused(&self) -> bool {
|
||||
return self.end_time.is_none();
|
||||
self.end_time.is_none()
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
@@ -71,24 +71,26 @@ impl Timer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main_loop(config: &mut Config, duration: &str) -> io::Result<()> {
|
||||
pub fn main_loop(config: &mut Config, duration: &str) -> io::Result<String> {
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
let duration = parse_duration::parse(duration).unwrap();
|
||||
let duration = parse_duration::parse(duration)
|
||||
.unwrap_or_else(|_| eprintln_quit!("Invalid duration provided"));
|
||||
let mut timer = Timer::new(duration);
|
||||
|
||||
let mut quit = false;
|
||||
while !quit {
|
||||
let mut quit_reason = None;
|
||||
while quit_reason.is_none() {
|
||||
// Handle events
|
||||
while event::poll(Duration::ZERO)? {
|
||||
match event::read()? {
|
||||
Event::Key(e) => match e.code {
|
||||
// Handle CTRL-C
|
||||
// Handle quit via CTRL-C or Q
|
||||
KeyCode::Char('c') => {
|
||||
if e.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
quit = true;
|
||||
quit_reason = Some("CTRL-C pressed");
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => quit_reason = Some("Q pressed"),
|
||||
// Handle pause
|
||||
KeyCode::Char(' ') => {
|
||||
timer.toggle_pause();
|
||||
@@ -116,7 +118,7 @@ pub fn main_loop(config: &mut Config, duration: &str) -> io::Result<()> {
|
||||
thread::sleep(Duration::from_millis(1000 / config.fps));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
Ok(quit_reason.unwrap().to_string())
|
||||
}
|
||||
|
||||
fn render_frame(config: &Config, timer: &Timer) -> io::Result<()> {
|
||||
@@ -146,5 +148,5 @@ fn render_frame(config: &Config, timer: &Timer) -> io::Result<()> {
|
||||
rendering::draw_text(text, x, y, color)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ pub struct ComputableColor {
|
||||
|
||||
impl ComputableColor {
|
||||
pub fn from(color: Color) -> ComputableColor {
|
||||
return ComputableColor {
|
||||
ComputableColor {
|
||||
values: vec![color],
|
||||
current: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self) -> () {
|
||||
@@ -18,27 +18,26 @@ impl ComputableColor {
|
||||
}
|
||||
|
||||
pub fn get_value(&self) -> Color {
|
||||
return *self.values.get(self.current).unwrap();
|
||||
*self.values.get(self.current).unwrap()
|
||||
}
|
||||
|
||||
pub fn get_keys_count(&self) -> usize {
|
||||
return self.values.len();
|
||||
self.values.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp01(v: f32) -> f32 {
|
||||
return if v < 0.0 {
|
||||
if v < 0.0 {
|
||||
0.0
|
||||
} else if v > 1.0 {
|
||||
1.0
|
||||
} else {
|
||||
v
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn lerp(a: u8, b: u8, t: f32) -> u8 {
|
||||
let v = a as f32 + (b as f32 - a as f32) as f32 * clamp01(t);
|
||||
return v as u8;
|
||||
(a as f32 + (b as f32 - a as f32) as f32 * clamp01(t)) as u8
|
||||
}
|
||||
|
||||
pub fn generate_gradient(keys: Vec<(u8, u8, u8)>, steps: usize) -> ComputableColor {
|
||||
@@ -61,10 +60,10 @@ pub fn generate_gradient(keys: Vec<(u8, u8, u8)>, steps: usize) -> ComputableCol
|
||||
}
|
||||
}
|
||||
|
||||
return ComputableColor {
|
||||
ComputableColor {
|
||||
values: gradient,
|
||||
current: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_hex_color(value: &str) -> (u8, u8, u8) {
|
||||
@@ -87,9 +86,14 @@ pub fn parse_hex_color(value: &str) -> (u8, u8, u8) {
|
||||
panic!("ERROR: Invalid hex color: {}", value);
|
||||
}
|
||||
|
||||
let r = u8::from_str_radix(&value[0..2], 16).unwrap();
|
||||
let g = u8::from_str_radix(&value[2..4], 16).unwrap();
|
||||
let b = u8::from_str_radix(&value[4..6], 16).unwrap();
|
||||
let extract_component = |index: usize| {
|
||||
u8::from_str_radix(&value[index * 2..(index + 1) * 2], 16)
|
||||
.unwrap_or_else(|_| panic!("error: invalid hex color: {}", value))
|
||||
};
|
||||
|
||||
return (r, g, b);
|
||||
(
|
||||
extract_component(0),
|
||||
extract_component(1),
|
||||
extract_component(2),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ pub fn draw_time(time: &str, color: Color) -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn draw_text(mut string: &str, mut x: i16, y: i16, color: Color) -> io::Result<()> {
|
||||
@@ -63,7 +63,8 @@ pub fn draw_text(mut string: &str, mut x: i16, y: i16, color: Color) -> io::Resu
|
||||
style::SetAttribute(Attribute::Bold)
|
||||
)?;
|
||||
write!(stdout, "{}", string)?;
|
||||
return Ok(());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_time_width(time: &str) -> i16 {
|
||||
@@ -113,5 +114,5 @@ fn draw_time_symbol(symbol: char, x: i16, y: i16, color: Color) -> io::Result<()
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
12
src/utils.rs
12
src/utils.rs
@@ -6,5 +6,15 @@ pub fn format_duration(duration: time::Duration) -> String {
|
||||
let minutes = (seconds % 3600) / 60;
|
||||
let seconds = seconds % 60;
|
||||
|
||||
return format!("{:02}:{:02}:{:02}", hours, minutes, seconds);
|
||||
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! eprintln_quit {
|
||||
($($arg:tt)*) => ({
|
||||
use std::io::Write;
|
||||
write!(&mut std::io::stderr(), "ERROR: ").unwrap();
|
||||
writeln!(&mut std::io::stderr(), $($arg)*).unwrap();
|
||||
std::process::exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user