Compare commits

...

10 Commits

Author SHA1 Message Date
Pihkaal
312b2f76e2 feat: handle Q for quitting 2025-12-02 14:10:14 +01:00
Pihkaal
a731360ea4 chore(readme): update 2024-09-13 17:47:02 +02:00
Pihkaal
882c12577d chore(license): format 2024-09-13 17:46:49 +02:00
Pihkaal
d9de028f0e chore: update license 2024-09-13 15:01:53 +02:00
Pihkaal
8146ffee44 feat(github): release workflow 2024-03-26 13:22:21 +01:00
Pihkaal
90a5ab038d chore: more informations in Cargo.toml 2024-03-26 13:15:23 +01:00
Pihkaal
1e6cf30fea feat: better error reporting 2024-03-25 23:50:20 +01:00
Pihkaal
ac66044508 chore: remove returns because it's bad code style (i've read that on stack overflow) 2024-03-23 05:24:29 +01:00
Pihkaal
3150fc3e23 chore: comments 2024-03-23 05:15:14 +01:00
Pihkaal
1bf0555d66 feat(cli): add subcommand aliases 2024-03-23 03:16:31 +01:00
13 changed files with 288 additions and 107 deletions

29
.github/workflows/release.yml vendored Normal file
View 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 }}

View File

@@ -2,6 +2,17 @@
name = "tlock" name = "tlock"
version = "0.1.0" version = "0.1.0"
edition = "2021" 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] [dependencies]
chrono = "0.4.31" chrono = "0.4.31"

View File

@@ -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 Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without 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 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE. OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -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.

View File

@@ -1,10 +1,10 @@
use core::panic; use std::{any::type_name, fs, path::PathBuf};
use std::{fs, path::PathBuf};
use crossterm::style::Color; use crossterm::style::Color;
use ini::configparser::ini::Ini; use ini::configparser::ini::Ini;
use crate::{ use crate::{
eprintln_quit,
modes::debug, modes::debug,
rendering::color::{generate_gradient, parse_hex_color, ComputableColor}, 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 { pub fn load_from_file(path: PathBuf, debug_mode: bool) -> Config {
let mut ini = Ini::new(); 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 { Config {
be_polite: ini.getbool("general", "polite").unwrap().unwrap(), be_polite: get_ini_value(&ini, "general", "polite"),
fps: ini.getuint("general", "fps").unwrap().unwrap(), fps: get_ini_value(&ini, "general", "fps"),
color: load_color(&ini, debug_mode), color: load_color(&ini, debug_mode),
time_format: ini.get("format", "time").unwrap(), time_format: get_ini_value(&ini, "format", "time"),
date_format: ini.get("format", "date").unwrap(), date_format: get_ini_value(&ini, "format", "date"),
}; }
return config;
} }
pub fn write_default_config(path: PathBuf) -> () { 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::create_dir_all(parent);
let _ = fs::write(path, DEFAULT_CONFIG); let _ = fs::write(path, DEFAULT_CONFIG);
} }
fn load_color(ini: &Ini, debug_mode: bool) -> ComputableColor { fn get_ini_value<T: std::str::FromStr>(ini: &Ini, section: &str, key: &str) -> T {
let color_mode = ini.get("styling", "color_mode").unwrap(); if let Some(value) = ini.get(section, key) {
value.parse::<T>().unwrap_or_else(|_| {
match color_mode.as_str() { eprintln_quit!(
"term" => { "Invalid value at {}.{}: Expected {}, got '{}'",
let color = ini.getint("styling", "color_term").unwrap().unwrap(); section,
return ComputableColor::from(load_term_color(color)); key,
} type_name::<T>(),
"hex" => { value
let color = ini.get("styling", "color_hex").unwrap(); )
return ComputableColor::from(load_hex_color(&color)); })
} } else {
"ansi" => { eprintln_quit!("Missing required config key: {}.{}", section, key)
let color = ini.getint("styling", "color_ansi").unwrap().unwrap();
return ComputableColor::from(load_ansi_color(color));
}
"gradient" => {
return load_gradient(ini, debug_mode);
}
_ => panic!("ERROR: Invalid color mode: {}", color_mode),
} }
} }
fn load_term_color(value: i64) -> Color { fn load_color(ini: &Ini, debug_mode: bool) -> ComputableColor {
let color_mode: String = get_ini_value(&ini, "styling", "color_mode");
match color_mode.as_str() {
"term" => {
let color: u8 = get_ini_value(&ini, "styling", "color_term");
ComputableColor::from(load_term_color(color))
}
"hex" => {
let color: String = get_ini_value(&ini, "styling", "color_hex");
ComputableColor::from(load_hex_color(&color))
}
"ansi" => {
let color: u8 = get_ini_value(&ini, "styling", "color_ansi");
ComputableColor::from(load_ansi_color(color))
}
"gradient" => load_gradient(ini, debug_mode),
_ => eprintln_quit!("Invalid color mode: {}", color_mode),
}
}
fn load_term_color(value: u8) -> Color {
match value { match value {
0 => Color::Black, 0 => Color::Black,
1 => Color::DarkRed, 1 => Color::DarkRed,
@@ -81,48 +101,48 @@ fn load_term_color(value: i64) -> Color {
13 => Color::Magenta, 13 => Color::Magenta,
14 => Color::Cyan, 14 => Color::Cyan,
15 => Color::White, 15 => Color::White,
_ => panic!("ERROR: Invalid terminal color: {}", value), _ => eprintln_quit!("Invalid terminal color: {}", value),
} }
} }
fn load_hex_color(value: &str) -> Color { fn load_hex_color(value: &str) -> Color {
let rgb = parse_hex_color(value); let rgb = parse_hex_color(value);
return Color::Rgb { Color::Rgb {
r: rgb.0, r: rgb.0,
g: rgb.1, g: rgb.1,
b: rgb.2, b: rgb.2,
}; }
} }
fn load_ansi_color(value: i64) -> Color { fn load_ansi_color(value: u8) -> Color {
return Color::AnsiValue(value.try_into().unwrap()); Color::AnsiValue(value)
} }
fn load_gradient(ini: &Ini, debug_mode: bool) -> ComputableColor { fn load_gradient(ini: &Ini, debug_mode: bool) -> ComputableColor {
let mut keys = Vec::new(); 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; let mut i = 0;
while let Some(key) = ini.get("gradient", &format!("gradient_key_{}", i)) { while let Some(key) = ini.get("gradient", &format!("gradient_key_{}", i)) {
keys.push(parse_hex_color(&key)); keys.push(parse_hex_color(&key));
i += 1; i += 1;
} }
if !debug_mode && ini.getbool("gradient", "gradient_loop").unwrap().unwrap() { // Generate gradient loop if needed
let mut loop_keys = keys.clone(); if !debug_mode && get_ini_value(&ini, "gradient", "gradient_loop") {
loop_keys.reverse(); for &key in keys.clone().iter().rev().skip(1) {
for i in 1..loop_keys.len() { keys.push(key);
keys.push(*loop_keys.get(i).unwrap());
} }
} }
// I use half characters for debug mode rendering, so we take display size * 2
let steps: usize = if debug_mode { let steps: usize = if debug_mode {
debug::DEBUG_COLOR_DISPLAY_SIZE * 2 debug::DEBUG_COLOR_DISPLAY_SIZE * 2
} else { } else {
ini.getuint("gradient", "gradient_steps") get_ini_value(&ini, "gradient", "gradient_steps")
.unwrap()
.unwrap()
.try_into()
.unwrap()
}; };
return generate_gradient(keys, steps - 1); generate_gradient(keys, steps - 1)
} }

View File

@@ -35,8 +35,13 @@ struct Cli {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum Commands { enum Commands {
#[clap(alias = "d")]
Debug {}, Debug {},
#[clap(alias = "c")]
Chrono {}, Chrono {},
#[clap(alias = "t")]
Timer { Timer {
#[arg(required = true)] #[arg(required = true)]
duration: Vec<String>, duration: Vec<String>,
@@ -46,12 +51,16 @@ enum Commands {
fn main() -> io::Result<()> { fn main() -> io::Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
// Load config // Load config from either given config file
let mut default_generated = false; let mut default_generated = false;
let config_file = if let Some(custom_config) = cli.config { let config_file = if let Some(custom_config) = cli.config {
PathBuf::from(custom_config) PathBuf::from(custom_config)
} else { } 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() { if !config_file.exists() {
write_default_config(config_file.clone()); write_default_config(config_file.clone());
default_generated = true; default_generated = true;
@@ -60,7 +69,10 @@ fn main() -> io::Result<()> {
config_file config_file
}; };
// Regenerate default config if needed
if cli.regenerate_default { 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 { if !default_generated && config_file.exists() && !cli.yes {
println!("A config file is already located at {:?}", config_file); println!("A config file is already located at {:?}", config_file);
print!("Do you really want to recreate it ? [y/N] "); 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()); write_default_config(config_file.clone());
println!("Done."); println!("Done.");
return Ok(()); return Ok(());
} }
// If no config file was found, throw an error
// NOTE: this should never happen
if !config_file.exists() { if !config_file.exists() {
panic!("ERROR: Configuration file not found"); panic!("ERROR: Configuration file not found");
} }
// Enable debug mode if needed, and load config
let debug_mode = match &cli.command { let debug_mode = match &cli.command {
Some(Commands::Debug {}) => true, Some(Commands::Debug {}) => true,
_ => false, _ => false,
@@ -93,6 +109,7 @@ fn main() -> io::Result<()> {
let mut config = config::load_from_file(config_file, debug_mode); let mut config = config::load_from_file(config_file, debug_mode);
let mut stdout = io::stdout(); let mut stdout = io::stdout();
// Print debug infos
if debug_mode { if debug_mode {
debug::print_debug_infos(&mut config)?; debug::print_debug_infos(&mut config)?;
return Ok(()); return Ok(());
@@ -102,7 +119,8 @@ fn main() -> io::Result<()> {
execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?; execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;
let _ = terminal::enable_raw_mode()?; 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::Chrono {}) => modes::chrono::main_loop(&mut config)?,
Some(Commands::Timer { duration }) => { Some(Commands::Timer { duration }) => {
let duration = duration.join(" "); let duration = duration.join(" ");
@@ -110,16 +128,16 @@ fn main() -> io::Result<()> {
} }
Some(Commands::Debug {}) => unreachable!(), Some(Commands::Debug {}) => unreachable!(),
None => modes::clock::main_loop(&mut config)?, None => modes::clock::main_loop(&mut config)?,
} };
// Disale raw mode, leave the alternate screen and show the cursor back // 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)?; execute!(stdout, terminal::LeaveAlternateScreen, cursor::Show)?;
// Be polite // Be polite
if config.be_polite { if config.be_polite {
println!("CTRL-C pressed, bye!\n"); println!("{}, bye!\n", quit_reason);
} }
return Ok(()); Ok(())
} }

View File

@@ -54,7 +54,7 @@ impl Chronometer {
} }
fn is_paused(&self) -> bool { fn is_paused(&self) -> bool {
return self.start_time.is_none(); self.start_time.is_none()
} }
fn elapsed(&self) -> Duration { 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 stdout = io::stdout();
let mut chronometer = Chronometer::new(); 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 lapses: Vec<Lapse> = vec![];
let mut scroll_offset: usize = 0; let mut scroll_offset: usize = 0;
let mut quit = false; let mut quit_reason = None;
while !quit { while quit_reason.is_none() {
// Handle events // Handle events
while event::poll(Duration::ZERO)? { while event::poll(Duration::ZERO)? {
match event::read()? { match event::read()? {
Event::Key(e) => match e.code { Event::Key(e) => match e.code {
// Handle CTRL-C // Handle quit via CTRL-C or Q
KeyCode::Char('c') => { KeyCode::Char('c') => {
if e.modifiers.contains(KeyModifiers::CONTROL) { if e.modifiers.contains(KeyModifiers::CONTROL) {
quit = true; quit_reason = Some("CTRL-C pressed");
} }
} }
KeyCode::Char('q') => quit_reason = Some("Q pressed"),
// Handle pause // Handle pause
KeyCode::Char(' ') => { KeyCode::Char(' ') => {
chronometer.toggle_pause(); chronometer.toggle_pause();
@@ -146,7 +147,7 @@ pub fn main_loop(config: &mut Config) -> io::Result<()> {
thread::sleep(Duration::from_millis(1000 / config.fps)); thread::sleep(Duration::from_millis(1000 / config.fps));
} }
return Ok(()); Ok(quit_reason.unwrap().to_string())
} }
fn render_frame( fn render_frame(
@@ -172,6 +173,7 @@ fn render_frame(
*scroll_offset = lapses.len() - max_items; *scroll_offset = lapses.len() - max_items;
} }
// Iterate over lapses, skipping with scroll offset and taxing N items
for (i, lapse) in lapses for (i, lapse) in lapses
.iter() .iter()
.rev() .rev()
@@ -201,5 +203,5 @@ fn render_frame(
rendering::draw_text(text, x, y, color)?; rendering::draw_text(text, x, y, color)?;
} }
return Ok(()); Ok(())
} }

View File

@@ -16,21 +16,22 @@ use crate::{
rendering::{self, symbols}, 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 stdout = io::stdout();
let mut quit = false; let mut quit_reason = None;
while !quit { while quit_reason.is_none() {
// Handle events // Handle events
while event::poll(Duration::ZERO)? { while event::poll(Duration::ZERO)? {
match event::read()? { match event::read()? {
Event::Key(e) => match e.code { Event::Key(e) => match e.code {
// Handle CTRL-C // Handle quit via CTRL-C or Q
KeyCode::Char('c') => { KeyCode::Char('c') => {
if e.modifiers.contains(KeyModifiers::CONTROL) { 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)); thread::sleep(Duration::from_millis(1000 / config.fps));
} }
return Ok(()); Ok(quit_reason.unwrap().to_string())
} }
fn render_frame(config: &Config) -> io::Result<()> { 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; let y = height / 2 + symbols::SYMBOL_HEIGHT as i16 / 2 + 2;
rendering::draw_text(&date, x, y - 1, color)?; rendering::draw_text(&date, x, y - 1, color)?;
return Ok(()); Ok(())
} }

View File

@@ -27,10 +27,15 @@ pub fn print_debug_infos(config: &mut Config) -> io::Result<()> {
print_debug_label("Color scheme")?; print_debug_label("Color scheme")?;
let width = config.color.get_keys_count(); let width = config.color.get_keys_count();
// If width is one, it is a single color
if width == 1 { if width == 1 {
queue!(stdout, style::SetBackgroundColor(config.color.get_value()))?; queue!(stdout, style::SetBackgroundColor(config.color.get_value()))?;
write!(stdout, "{}", " ".repeat(DEBUG_COLOR_DISPLAY_SIZE))?; 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 { for _ in 0..width / 2 {
queue!(stdout, style::SetForegroundColor(config.color.get_value()))?; queue!(stdout, style::SetForegroundColor(config.color.get_value()))?;
config.color.update(); config.color.update();
@@ -45,7 +50,8 @@ pub fn print_debug_infos(config: &mut Config) -> io::Result<()> {
writeln!(stdout)?; writeln!(stdout)?;
queue!(stdout, style::ResetColor)?; queue!(stdout, style::ResetColor)?;
let _ = stdout.flush(); let _ = stdout.flush();
return Ok(());
Ok(())
} }
fn print_debug_label(key: &str) -> io::Result<()> { fn print_debug_label(key: &str) -> io::Result<()> {
@@ -55,5 +61,5 @@ fn print_debug_label(key: &str) -> io::Result<()> {
write!(stdout, "{}: ", key)?; write!(stdout, "{}: ", key)?;
queue!(stdout, style::ResetColor)?; queue!(stdout, style::ResetColor)?;
return Ok(()); Ok(())
} }

View File

@@ -10,11 +10,11 @@ use crossterm::{
terminal::{self, ClearType}, terminal::{self, ClearType},
}; };
use crate::utils;
use crate::{ use crate::{
config::Config, config::Config,
rendering::{self, symbols}, rendering::{self, symbols},
}; };
use crate::{eprintln_quit, utils};
struct Timer { struct Timer {
duration: Duration, duration: Duration,
@@ -61,7 +61,7 @@ impl Timer {
} }
fn is_paused(&self) -> bool { fn is_paused(&self) -> bool {
return self.end_time.is_none(); self.end_time.is_none()
} }
fn reset(&mut self) { 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 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 timer = Timer::new(duration);
let mut quit = false; let mut quit_reason = None;
while !quit { while quit_reason.is_none() {
// Handle events // Handle events
while event::poll(Duration::ZERO)? { while event::poll(Duration::ZERO)? {
match event::read()? { match event::read()? {
Event::Key(e) => match e.code { Event::Key(e) => match e.code {
// Handle CTRL-C // Handle quit via CTRL-C or Q
KeyCode::Char('c') => { KeyCode::Char('c') => {
if e.modifiers.contains(KeyModifiers::CONTROL) { if e.modifiers.contains(KeyModifiers::CONTROL) {
quit = true; quit_reason = Some("CTRL-C pressed");
} }
} }
KeyCode::Char('q') => quit_reason = Some("Q pressed"),
// Handle pause // Handle pause
KeyCode::Char(' ') => { KeyCode::Char(' ') => {
timer.toggle_pause(); 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)); 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<()> { 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)?; rendering::draw_text(text, x, y, color)?;
} }
return Ok(()); Ok(())
} }

View File

@@ -7,10 +7,10 @@ pub struct ComputableColor {
impl ComputableColor { impl ComputableColor {
pub fn from(color: Color) -> ComputableColor { pub fn from(color: Color) -> ComputableColor {
return ComputableColor { ComputableColor {
values: vec![color], values: vec![color],
current: 0, current: 0,
}; }
} }
pub fn update(&mut self) -> () { pub fn update(&mut self) -> () {
@@ -18,27 +18,26 @@ impl ComputableColor {
} }
pub fn get_value(&self) -> Color { 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 { pub fn get_keys_count(&self) -> usize {
return self.values.len(); self.values.len()
} }
} }
fn clamp01(v: f32) -> f32 { fn clamp01(v: f32) -> f32 {
return if v < 0.0 { if v < 0.0 {
0.0 0.0
} else if v > 1.0 { } else if v > 1.0 {
1.0 1.0
} else { } else {
v v
}; }
} }
fn lerp(a: u8, b: u8, t: f32) -> u8 { fn lerp(a: u8, b: u8, t: f32) -> u8 {
let v = a as f32 + (b as f32 - a as f32) as f32 * clamp01(t); (a as f32 + (b as f32 - a as f32) as f32 * clamp01(t)) as u8
return v as u8;
} }
pub fn generate_gradient(keys: Vec<(u8, u8, u8)>, steps: usize) -> ComputableColor { 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, values: gradient,
current: 0, current: 0,
}; }
} }
pub fn parse_hex_color(value: &str) -> (u8, u8, u8) { 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); panic!("ERROR: Invalid hex color: {}", value);
} }
let r = u8::from_str_radix(&value[0..2], 16).unwrap(); let extract_component = |index: usize| {
let g = u8::from_str_radix(&value[2..4], 16).unwrap(); u8::from_str_radix(&value[index * 2..(index + 1) * 2], 16)
let b = u8::from_str_radix(&value[4..6], 16).unwrap(); .unwrap_or_else(|_| panic!("error: invalid hex color: {}", value))
};
return (r, g, b); (
extract_component(0),
extract_component(1),
extract_component(2),
)
} }

View File

@@ -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<()> { 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) style::SetAttribute(Attribute::Bold)
)?; )?;
write!(stdout, "{}", string)?; write!(stdout, "{}", string)?;
return Ok(());
Ok(())
} }
fn draw_time_width(time: &str) -> i16 { 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(())
} }

View File

@@ -6,5 +6,15 @@ pub fn format_duration(duration: time::Duration) -> String {
let minutes = (seconds % 3600) / 60; let minutes = (seconds % 3600) / 60;
let seconds = seconds % 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)
})
} }