Initial commit.
This commit is contained in:
commit
c04bde3ba4
8 changed files with 1182 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
\.*.swp
|
57
Cargo.lock
generated
Normal file
57
Cargo.lock
generated
Normal file
|
@ -0,0 +1,57 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||
|
||||
[[package]]
|
||||
name = "kt"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"termion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7"
|
||||
|
||||
[[package]]
|
||||
name = "numtoa"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
|
||||
dependencies = [
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "1.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"numtoa",
|
||||
"redox_syscall",
|
||||
"redox_termios",
|
||||
]
|
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "kt"
|
||||
version = "0.1.0"
|
||||
authors = ["shy <shy@posteo.de>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
termion = "1.5"
|
||||
libc = "0.2"
|
280
src/alarm.rs
Normal file
280
src/alarm.rs
Normal file
|
@ -0,0 +1,280 @@
|
|||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
use termion::{color, cursor, style};
|
||||
use termion::raw::RawTerminal;
|
||||
use crate::{Clock, Config, Layout, Position};
|
||||
use crate::common::COLOR;
|
||||
|
||||
|
||||
pub struct Countdown {
|
||||
pub value: u32,
|
||||
position: Option<Position>,
|
||||
}
|
||||
|
||||
impl Countdown {
|
||||
pub fn new() -> Countdown {
|
||||
Countdown {
|
||||
value: 0,
|
||||
position: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.value = 0;
|
||||
}
|
||||
|
||||
// Draw countdown.
|
||||
pub fn draw<W: Write>(&self, stdout: &mut RawTerminal<W>) {
|
||||
if let Some(pos) = &self.position {
|
||||
if self.value < 3600 {
|
||||
// Show minutes and seconds.
|
||||
write!(stdout,
|
||||
"{}(-{:02}:{:02})",
|
||||
cursor::Goto(pos.col, pos.line),
|
||||
(self.value / 60) % 60,
|
||||
self.value % 60)
|
||||
.unwrap();
|
||||
if self.value == 3599 {
|
||||
// Write three additional spaces after switching from hour display to
|
||||
// minute display.
|
||||
write!(stdout, " ").unwrap();
|
||||
}
|
||||
} else {
|
||||
// Show hours, minutes and seconds.
|
||||
write!(stdout,
|
||||
"{}(-{:02}:{:02}:{:02})",
|
||||
cursor::Goto(pos.col, pos.line),
|
||||
self.value / 3600,
|
||||
(self.value / 60) % 60,
|
||||
self.value % 60)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Alarm {
|
||||
time: u32,
|
||||
display: String,
|
||||
color_index: usize,
|
||||
exceeded: bool,
|
||||
}
|
||||
|
||||
impl Alarm {
|
||||
fn reset(&mut self) {
|
||||
self.exceeded = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AlarmRoster {
|
||||
list: Vec<Alarm>,
|
||||
}
|
||||
|
||||
impl AlarmRoster {
|
||||
pub fn new() -> AlarmRoster {
|
||||
AlarmRoster {
|
||||
list: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, buffer: &String)
|
||||
-> Result<(), &'static str> {
|
||||
|
||||
let mut index = 0;
|
||||
let mut time: u32 = 0;
|
||||
|
||||
// Parse input into seconds.
|
||||
for sub in buffer.rsplit(':') {
|
||||
if sub.len() > 0 {
|
||||
let d = sub.parse::<u32>();
|
||||
match d {
|
||||
Ok(d) => time += d * 60u32.pow(index),
|
||||
Err(_) => return Err("Could not parse number as <u32>."),
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
|
||||
// More than 3 fields are an error.
|
||||
if index > 3 { return Err("Too many colons to parse.") };
|
||||
}
|
||||
|
||||
// Skip if time evaluated to zero.
|
||||
if time == 0 { return Err("Evaluates to zero.") };
|
||||
if time >= 24 * 60 * 60 { return Err("Values >24h not supported.") };
|
||||
|
||||
let alarm = Alarm {
|
||||
display: buffer.clone(),
|
||||
time,
|
||||
color_index: (self.list.len() % COLOR.len()),
|
||||
exceeded: false,
|
||||
};
|
||||
|
||||
// Add to list, insert based on alarm time. Filter out double entries.
|
||||
let mut i = self.list.len();
|
||||
if i == 0 {
|
||||
self.list.push(alarm);
|
||||
} else {
|
||||
while i > 0 {
|
||||
// Filter out double entries.
|
||||
if self.list[i - 1].time == time {
|
||||
return Err("Already exists.");
|
||||
} else if self.list[i - 1].time < time {
|
||||
break;
|
||||
}
|
||||
i -= 1;
|
||||
}
|
||||
self.list.insert(i, alarm);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<Alarm> {
|
||||
self.list.pop()
|
||||
}
|
||||
|
||||
// Check for exceeded alarms.
|
||||
pub fn check(&mut self,
|
||||
clock: &mut Clock,
|
||||
layout: &Layout,
|
||||
countdown: &mut Countdown) -> bool {
|
||||
|
||||
let mut hit = false;
|
||||
let mut index = 0;
|
||||
|
||||
for alarm in &mut self.list {
|
||||
// Ignore alarms already marked exceeded.
|
||||
if !alarm.exceeded {
|
||||
if alarm.time <= clock.elapsed {
|
||||
// Found alarm to raise.
|
||||
hit = true;
|
||||
alarm.exceeded = true;
|
||||
clock.color_index = Some(alarm.color_index);
|
||||
countdown.value = 0;
|
||||
countdown.position = None;
|
||||
// Skip ahead to the next one.
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
// Reached the alarm to exceed next. Update countdown
|
||||
// accordingly.
|
||||
countdown.value = alarm.time - clock.elapsed;
|
||||
if countdown.position.is_none() || layout.force_redraw {
|
||||
// Compute position.
|
||||
let mut col =
|
||||
layout.roster.col
|
||||
+ 3
|
||||
+ alarm.display.len() as u16;
|
||||
let mut line = layout.roster.line + index;
|
||||
|
||||
// Compensate for "hidden" items in the alarm roster.
|
||||
// TODO: Make this more elegant and robust.
|
||||
if let Some(offset) = (self.list.len() as u16)
|
||||
.checked_sub(layout.roster_height + 1) {
|
||||
|
||||
if index <= offset{
|
||||
// Draw next to placeholder ("[...]").
|
||||
line = layout.roster.line;
|
||||
col = layout.roster.col + 6;
|
||||
} else {
|
||||
line = line.checked_sub(offset).unwrap_or(layout.roster.line);
|
||||
}
|
||||
}
|
||||
countdown.position = Some(Position { col, line, });
|
||||
}
|
||||
// Ignore other alarms.
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
hit // Return value.
|
||||
}
|
||||
|
||||
// Draw alarm roster according to layout.
|
||||
pub fn draw<W: Write>(&self, stdout: &mut RawTerminal<W>, layout: &mut Layout) {
|
||||
let mut width: u16 = 0;
|
||||
let mut index = 0;
|
||||
|
||||
// Find first item to print in case we lack space to print them all.
|
||||
// Final '-1' to take account for the input buffer.
|
||||
let mut first = 0;
|
||||
|
||||
if self.list.len() > layout.roster_height as usize {
|
||||
// Actually -1 (zero indexing) +1 (first line containing "...").
|
||||
first = self.list.len() - layout.roster_height as usize;
|
||||
index += 1;
|
||||
|
||||
write!(stdout,
|
||||
"{}{}[...]{}",
|
||||
cursor::Goto(layout.roster.col, layout.roster.line),
|
||||
style::Faint,
|
||||
style::Reset).unwrap();
|
||||
}
|
||||
|
||||
for alarm in &self.list[first..] {
|
||||
if alarm.exceeded {
|
||||
write!(stdout,
|
||||
"{}{} {}{} {}!{}",
|
||||
cursor::Goto(layout.roster.col, layout.roster.line + index),
|
||||
color::Bg(COLOR[alarm.color_index]),
|
||||
color::Bg(color::Reset),
|
||||
style::Bold,
|
||||
alarm.display,
|
||||
style::Reset)
|
||||
.unwrap();
|
||||
} else {
|
||||
write!(stdout,
|
||||
"{}{} {} {}",
|
||||
cursor::Goto(layout.roster.col, layout.roster.line + index),
|
||||
color::Bg(COLOR[alarm.color_index]),
|
||||
color::Bg(color::Reset),
|
||||
alarm.display)
|
||||
.unwrap();
|
||||
}
|
||||
index += 1;
|
||||
// Calculate roster width. Actual display width is 3 chars wider.
|
||||
if 3 + alarm.display.len() as u16 > width {
|
||||
width = 3 + alarm.display.len() as u16;
|
||||
}
|
||||
}
|
||||
// Update layout information.
|
||||
if layout.roster_width != width {
|
||||
layout.roster_width = width;
|
||||
layout.force_recalc = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset every alarm.
|
||||
pub fn reset_all(&mut self) {
|
||||
for a in &mut self.list {
|
||||
a.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the command given on the command line.
|
||||
pub fn alarm_exec(config: &Config, elapsed: u32) {
|
||||
let mut args: Vec<String> = Vec::new();
|
||||
let time: String;
|
||||
|
||||
if elapsed < 3600 {
|
||||
time = format!("{:02}:{:02}", elapsed / 60, elapsed % 60);
|
||||
} else {
|
||||
time = format!("{:02}:{:02}:{:02}", elapsed /3600, (elapsed / 60) % 60, elapsed % 60);
|
||||
}
|
||||
|
||||
// Replace every occurrence of "%s".
|
||||
for s in config.alarm_exec.iter() {
|
||||
args.push(s.replace("%s", &time));
|
||||
}
|
||||
|
||||
if Command::new(
|
||||
&config.alarm_exec[0])
|
||||
.args(&args[1..])
|
||||
.stdout(Stdio::null())
|
||||
.stdin(Stdio::null())
|
||||
.spawn().is_err() {
|
||||
|
||||
eprintln!("Error: Could not execute command");
|
||||
}
|
||||
}
|
||||
|
209
src/clock.rs
Normal file
209
src/clock.rs
Normal file
|
@ -0,0 +1,209 @@
|
|||
use std::time;
|
||||
use std::io::Write;
|
||||
use termion::{color, cursor};
|
||||
use termion::raw::RawTerminal;
|
||||
use crate::common::*;
|
||||
use crate::Layout;
|
||||
use crate::Position;
|
||||
use crate::common::COLOR;
|
||||
|
||||
|
||||
pub struct Clock {
|
||||
pub start: time::Instant,
|
||||
pub elapsed: u32,
|
||||
pub days: u32,
|
||||
pub paused: bool,
|
||||
paused_at: Option<time::Instant>,
|
||||
pub color_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl Clock {
|
||||
pub fn new() -> Clock {
|
||||
Clock {
|
||||
start: time::Instant::now(),
|
||||
elapsed: 0,
|
||||
days: 0,
|
||||
paused: false,
|
||||
paused_at: None,
|
||||
color_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.start = time::Instant::now();
|
||||
self.elapsed = 0;
|
||||
self.days = 0;
|
||||
self.color_index = None;
|
||||
|
||||
// unpause will panic if we do not trigger a new pause here.
|
||||
if self.paused {
|
||||
self.pause();
|
||||
}
|
||||
}
|
||||
|
||||
fn pause(&mut self) {
|
||||
self.paused_at = Some(time::Instant::now());
|
||||
self.paused = true;
|
||||
}
|
||||
|
||||
fn unpause(&mut self) {
|
||||
// Try to derive a new start instant.
|
||||
if let Some(delay) = self.paused_at {
|
||||
if let Some(new_start) = self.start.checked_add(delay.elapsed()) {
|
||||
self.start = new_start;
|
||||
}
|
||||
}
|
||||
|
||||
self.paused_at = None;
|
||||
self.paused = false;
|
||||
}
|
||||
|
||||
pub fn toggle(&mut self) {
|
||||
if self.paused {
|
||||
self.unpause();
|
||||
} else {
|
||||
self.pause();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_day(&mut self) {
|
||||
// Shift start 24h into the future.
|
||||
let next = self.start.clone() + time::Duration::from_secs(60 * 60 * 24);
|
||||
|
||||
// Take care not to shift start into the future.
|
||||
if next <= time::Instant::now() {
|
||||
self.start = next;
|
||||
self.elapsed = 0;
|
||||
self.days = self.days.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw clock according to layout.
|
||||
pub fn draw<W: Write>(&mut self, mut stdout: &mut RawTerminal<W>, layout: &Layout) {
|
||||
// Draw hours if necessary.
|
||||
if layout.force_redraw || self.elapsed % 3600 == 0 {
|
||||
if self.elapsed >= 3600 {
|
||||
self.draw_digit_pair(
|
||||
&mut stdout,
|
||||
self.elapsed / 3600,
|
||||
&layout.clock_hr,
|
||||
layout.plain);
|
||||
|
||||
// Draw colon.
|
||||
self.draw_colon(
|
||||
&mut stdout,
|
||||
&layout.clock_colon1,
|
||||
layout.plain);
|
||||
}
|
||||
|
||||
// Draw days.
|
||||
if self.days > 0 {
|
||||
let day_count = format!(
|
||||
"+ {} {}",
|
||||
self.days,
|
||||
if self.days == 1 { "day" } else { "days" });
|
||||
|
||||
write!(stdout,
|
||||
"{}{:>11}",
|
||||
cursor::Goto(
|
||||
layout.clock_days.col,
|
||||
layout.clock_days.line,
|
||||
),
|
||||
day_count)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Draw minutes if necessary.
|
||||
if layout.force_redraw || self.elapsed % 60 == 0 {
|
||||
self.draw_digit_pair(
|
||||
&mut stdout,
|
||||
(self.elapsed % 3600) / 60,
|
||||
&layout.clock_min,
|
||||
layout.plain);
|
||||
}
|
||||
|
||||
// Draw colon if necessary.
|
||||
if layout.force_redraw {
|
||||
self.draw_colon(
|
||||
&mut stdout,
|
||||
&layout.clock_colon0,
|
||||
layout.plain);
|
||||
}
|
||||
|
||||
// Draw seconds.
|
||||
self.draw_digit_pair(
|
||||
&mut stdout,
|
||||
self.elapsed % 60,
|
||||
&layout.clock_sec,
|
||||
layout.plain);
|
||||
}
|
||||
|
||||
fn draw_digit_pair<W: Write>(&self, stdout: &mut RawTerminal<W>, value: u32, pos: &Position, plain: bool) {
|
||||
if let Some(c) = self.color_index {
|
||||
write!(stdout,
|
||||
"{}{}",
|
||||
cursor::Goto(pos.col, pos.line),
|
||||
color::Fg(COLOR[c]))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for l in 0..DIGIT_HEIGHT {
|
||||
if plain {
|
||||
write!(stdout,
|
||||
"{}{} {}",
|
||||
cursor::Goto(pos.col, pos.line + l),
|
||||
// First digit.
|
||||
DIGITS_PLAIN[(value / 10) as usize][l as usize],
|
||||
// Second digit.
|
||||
DIGITS_PLAIN[(value % 10) as usize][l as usize])
|
||||
.unwrap();
|
||||
} else {
|
||||
write!(stdout,
|
||||
"{}{} {}",
|
||||
cursor::Goto(pos.col, pos.line + l),
|
||||
// First digit.
|
||||
DIGITS[(value / 10) as usize][l as usize],
|
||||
// Second digit.
|
||||
DIGITS[(value % 10) as usize][l as usize])
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if self.color_index != None {
|
||||
write!(stdout,
|
||||
"{}{}",
|
||||
cursor::Goto(pos.col + DIGIT_WIDTH + 1, pos.line + DIGIT_HEIGHT),
|
||||
color::Fg(color::Reset))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_colon<W: Write>(&self, stdout: &mut RawTerminal<W>, pos: &Position, plain: bool) {
|
||||
let dot: char = if plain {'█'} else {'■'};
|
||||
|
||||
match self.color_index {
|
||||
Some(c) => {
|
||||
write!(stdout,
|
||||
"{}{}{}{}{}{}",
|
||||
cursor::Goto(pos.col, pos.line + 1),
|
||||
color::Fg(COLOR[c]),
|
||||
dot,
|
||||
cursor::Goto(pos.col, pos.line + 3),
|
||||
dot,
|
||||
color::Fg(color::Reset))
|
||||
.unwrap();
|
||||
}
|
||||
None => {
|
||||
write!(stdout,
|
||||
"{}{}{}{}",
|
||||
cursor::Goto(pos.col, pos.line + 1),
|
||||
dot,
|
||||
cursor::Goto(pos.col, pos.line + 3),
|
||||
dot)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
160
src/common.rs
Normal file
160
src/common.rs
Normal file
|
@ -0,0 +1,160 @@
|
|||
use termion::color;
|
||||
|
||||
pub const COLOR: [&dyn color::Color; 6] = [
|
||||
&color::Cyan,
|
||||
&color::Magenta,
|
||||
&color::Green,
|
||||
&color::Yellow,
|
||||
&color::Blue,
|
||||
&color::Red,
|
||||
];
|
||||
pub const DIGIT_HEIGHT: u16 = 5;
|
||||
pub const DIGIT_WIDTH: u16 = 5;
|
||||
pub const DIGITS: [[&str; DIGIT_HEIGHT as usize]; 10] = [
|
||||
[
|
||||
// 0
|
||||
"█▀▀▀█",
|
||||
"█ █",
|
||||
"█ █",
|
||||
"█ █",
|
||||
"█▄▄▄█",
|
||||
], [
|
||||
// 1
|
||||
" ▀█ ",
|
||||
" █ ",
|
||||
" █ ",
|
||||
" █ ",
|
||||
" █ "
|
||||
], [
|
||||
// 2
|
||||
"▀▀▀▀█",
|
||||
" █",
|
||||
"█▀▀▀▀",
|
||||
"█ ",
|
||||
"█▄▄▄▄"
|
||||
], [
|
||||
// 3
|
||||
"▀▀▀▀█",
|
||||
" █",
|
||||
" ▀▀▀█",
|
||||
" █",
|
||||
"▄▄▄▄█"
|
||||
], [
|
||||
// 4
|
||||
"█ ",
|
||||
"█ █ ",
|
||||
"▀▀▀█▀",
|
||||
" █ ",
|
||||
" █ "
|
||||
], [
|
||||
// 5
|
||||
"█▀▀▀▀",
|
||||
"█ ",
|
||||
"▀▀▀▀█",
|
||||
" █",
|
||||
"▄▄▄▄█"
|
||||
], [
|
||||
// 6
|
||||
"█ ",
|
||||
"█ ",
|
||||
"█▀▀▀█",
|
||||
"█ █",
|
||||
"█▄▄▄█"
|
||||
], [
|
||||
// 7
|
||||
"▀▀▀▀█",
|
||||
" █",
|
||||
" █ ",
|
||||
" █ ",
|
||||
" █ ",
|
||||
], [
|
||||
// 8
|
||||
"█▀▀▀█",
|
||||
"█ █",
|
||||
"█▀▀▀█",
|
||||
"█ █",
|
||||
"█▄▄▄█"
|
||||
], [
|
||||
// 9
|
||||
"█▀▀▀█",
|
||||
"█ █",
|
||||
"▀▀▀▀█",
|
||||
" █",
|
||||
" █"
|
||||
]
|
||||
];
|
||||
|
||||
pub const DIGITS_PLAIN: [[&str; DIGIT_HEIGHT as usize]; 10] = [
|
||||
[
|
||||
// 0
|
||||
"█████",
|
||||
"█ █",
|
||||
"█ █",
|
||||
"█ █",
|
||||
"█████"
|
||||
], [
|
||||
// 1
|
||||
" ██ ",
|
||||
" █ ",
|
||||
" █ ",
|
||||
" █ ",
|
||||
" █ "
|
||||
], [
|
||||
// 2
|
||||
"█████",
|
||||
" █",
|
||||
"█████",
|
||||
"█ ",
|
||||
"█████"
|
||||
], [
|
||||
// 3
|
||||
"█████",
|
||||
" █",
|
||||
" ████",
|
||||
" █",
|
||||
"█████"
|
||||
], [
|
||||
// 4
|
||||
"█ ",
|
||||
"█ █ ",
|
||||
"█████",
|
||||
" █ ",
|
||||
" █ "
|
||||
], [
|
||||
// 5
|
||||
"█████",
|
||||
"█ ",
|
||||
"█████",
|
||||
" █",
|
||||
"█████"
|
||||
], [
|
||||
// 6
|
||||
"█ ",
|
||||
"█ ",
|
||||
"█████",
|
||||
"█ █",
|
||||
"█████"
|
||||
], [
|
||||
// 7
|
||||
"█████",
|
||||
" █",
|
||||
" █ ",
|
||||
" █ ",
|
||||
" █ "
|
||||
], [
|
||||
// 8
|
||||
"█████",
|
||||
"█ █",
|
||||
"█████",
|
||||
"█ █",
|
||||
"█████"
|
||||
], [
|
||||
// 9
|
||||
"█████",
|
||||
"█ █",
|
||||
"█████",
|
||||
" █",
|
||||
" █"
|
||||
]
|
||||
];
|
||||
|
130
src/layout.rs
Normal file
130
src/layout.rs
Normal file
|
@ -0,0 +1,130 @@
|
|||
use crate::Config;
|
||||
use crate::common::*;
|
||||
|
||||
// If screen size falls below these values we skip computation of new
|
||||
// positions.
|
||||
const MIN_WIDTH: u16 = DIGIT_WIDTH * 6 + 13;
|
||||
const MIN_HEIGHT: u16 = DIGIT_HEIGHT + 2;
|
||||
|
||||
pub struct Position {
|
||||
pub line: u16,
|
||||
pub col: u16,
|
||||
}
|
||||
|
||||
pub struct Layout {
|
||||
pub force_redraw: bool, // Redraw elements on screen.
|
||||
pub force_recalc: bool, // Recalculate position of elements.
|
||||
pub plain: bool, // Plain style clock.
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub clock_sec: Position,
|
||||
pub clock_colon0: Position,
|
||||
pub clock_min: Position,
|
||||
pub clock_colon1: Position,
|
||||
pub clock_hr: Position,
|
||||
pub clock_days: Position,
|
||||
pub roster: Position,
|
||||
pub roster_width: u16,
|
||||
pub roster_height: u16,
|
||||
pub buffer: Position,
|
||||
pub error: Position,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn new(config: &Config) -> Layout {
|
||||
Layout {
|
||||
force_redraw: true,
|
||||
force_recalc: false,
|
||||
plain: config.plain,
|
||||
width: 0,
|
||||
height: 0,
|
||||
clock_sec: Position {col: 0, line: 0},
|
||||
clock_colon0: Position {col: 0, line: 0},
|
||||
clock_min: Position {col: 0, line: 0},
|
||||
clock_colon1: Position {col: 0, line: 0},
|
||||
clock_hr: Position {col: 0, line: 0},
|
||||
clock_days: Position {col: 0, line: 0},
|
||||
roster: Position {col: 1, line: 3},
|
||||
roster_width: 0,
|
||||
roster_height: 0,
|
||||
buffer: Position {col: 0, line: 0},
|
||||
error: Position {col: 0, line: 0},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, display_hours: bool) {
|
||||
let (width, height) = termion::terminal_size()
|
||||
.expect("Could not read terminal size!");
|
||||
|
||||
if self.force_recalc || self.width != width || self.height != height {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self.compute(display_hours);
|
||||
self.force_redraw = true;
|
||||
self.force_recalc = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the position of various elements based on the size of the
|
||||
// terminal.
|
||||
fn compute(&mut self, display_hours: bool) {
|
||||
let middle: u16 = self.height / 2 - 1;
|
||||
|
||||
// Prevent integer overflow on very low screen sizes.
|
||||
if self.width < MIN_WIDTH || self.height < MIN_HEIGHT { return; }
|
||||
|
||||
if display_hours {
|
||||
// Seconds digits.
|
||||
self.clock_sec.col = (self.width + self.roster_width) / 2 + DIGIT_WIDTH + 6;
|
||||
// Colon separating minutes from seconds.
|
||||
self.clock_colon0.col = (self.width + self.roster_width) / 2 + DIGIT_WIDTH + 3;
|
||||
// Minute digits.
|
||||
self.clock_min.col = (self.width + self.roster_width) / 2 - DIGIT_WIDTH;
|
||||
|
||||
// Colon separating hours from minutes.
|
||||
self.clock_colon1 = Position {
|
||||
col: (self.width + self.roster_width) / 2 - (DIGIT_WIDTH + 3),
|
||||
line: middle,
|
||||
};
|
||||
|
||||
// Hour digits.
|
||||
self.clock_hr = Position {
|
||||
col: (self.width + self.roster_width) / 2 - (DIGIT_WIDTH * 3 + 6),
|
||||
line: middle,
|
||||
};
|
||||
} else {
|
||||
// Seconds digits.
|
||||
self.clock_sec.col = (self.width + self.roster_width) / 2 + 3;
|
||||
// Colon separating minutes from seconds.
|
||||
self.clock_colon0.col = (self.width + self.roster_width) / 2;
|
||||
// Minute digits.
|
||||
self.clock_min.col = (self.width + self.roster_width) / 2 - (DIGIT_WIDTH * 2 + 3);
|
||||
}
|
||||
|
||||
self.clock_sec.line = middle;
|
||||
self.clock_colon0.line = middle;
|
||||
self.clock_min.line = middle;
|
||||
|
||||
// Days (based on position of seconds).
|
||||
self.clock_days = Position {
|
||||
line: self.clock_sec.line + DIGIT_HEIGHT + 1,
|
||||
col: self.clock_sec.col,
|
||||
};
|
||||
|
||||
// Alarm roster height.
|
||||
self.roster_height = self.height - self.roster.line - 1;
|
||||
|
||||
// Input buffer.
|
||||
self.buffer = Position {
|
||||
line: self.height,
|
||||
col: 1,
|
||||
};
|
||||
|
||||
// Error messages.
|
||||
self.error = Position {
|
||||
line: self.height,
|
||||
col: 12,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
333
src/main.rs
Normal file
333
src/main.rs
Normal file
|
@ -0,0 +1,333 @@
|
|||
extern crate termion;
|
||||
extern crate libc;
|
||||
mod alarm;
|
||||
mod clock;
|
||||
mod common;
|
||||
mod layout;
|
||||
|
||||
use std::{time, thread, env};
|
||||
use std::io::Write;
|
||||
use termion::{clear, color, cursor, style};
|
||||
use termion::raw::{IntoRawMode, RawTerminal};
|
||||
use termion::event::Key;
|
||||
use termion::input::TermRead;
|
||||
use clock::Clock;
|
||||
use alarm::{Countdown, AlarmRoster, alarm_exec};
|
||||
use layout::{Layout, Position};
|
||||
|
||||
|
||||
const NAME: &str = "kt (kitchentime)";
|
||||
const VERSION: &str = "0.0.1";
|
||||
const USAGE: &str =
|
||||
"USAGE: kt [-h|--help] [-v|--version] [-p|--plain] [-e|--exec COMMAND [...]]
|
||||
-p, --plain Use simpler block chars.
|
||||
-e, --exec [COMMAND] Execute \"COMMAND\" on alarm. Must be the last flag on
|
||||
the command line. Everything after it is passed as
|
||||
argument to \"COMMAND\". Every \"%s\" will be replaced
|
||||
with the elapsed time in [(HH:)MM:SS] format.";
|
||||
const MENUBAR: &str =
|
||||
"[0-9] Add alarm [d] Delete alarm [SPACE] Pause [r] Reset [c] Clear color [q] Quit";
|
||||
const MENUBAR_SHORT: &str =
|
||||
"[0-9] Add [d] Delete [SPACE] Pause [r] Reset [c] Clear [q] Quit";
|
||||
|
||||
pub struct Config {
|
||||
plain: bool,
|
||||
alarm_exec: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
fn main() {
|
||||
let mut config = Config {
|
||||
plain: false,
|
||||
alarm_exec: Vec::new(),
|
||||
};
|
||||
parse_args(&mut config);
|
||||
|
||||
let mut stdout = std::io::stdout().into_raw_mode()
|
||||
.unwrap_or_else(|error| {
|
||||
eprintln!("Error opening stdout: {}", error);
|
||||
std::process::exit(1);
|
||||
});
|
||||
let mut input_keys = termion::async_stdin().keys();
|
||||
let mut layout = Layout::new(&config);
|
||||
let mut clock = Clock::new();
|
||||
let mut alarm_roster = AlarmRoster::new();
|
||||
let mut buffer = String::new();
|
||||
let mut buffer_updated: bool = false;
|
||||
let mut countdown = Countdown::new();
|
||||
|
||||
// Clear screen and hide cursor.
|
||||
write!(stdout,
|
||||
"{}{}",
|
||||
clear::All,
|
||||
cursor::Hide)
|
||||
.unwrap_or_else(|error| {
|
||||
eprintln!("Error writing to stdout: {}", error);
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
loop {
|
||||
// Process input.
|
||||
if let Some(key) = input_keys.next() {
|
||||
match key.expect("Error reading input") {
|
||||
// Reset clock on 'r'.
|
||||
Key::Char('r') => {
|
||||
clock.reset();
|
||||
alarm_roster.reset_all();
|
||||
layout.force_redraw = true;
|
||||
},
|
||||
// (Un-)Pause on space.
|
||||
Key::Char(' ') => {
|
||||
clock.toggle();
|
||||
},
|
||||
// Clear clock color on 'c'.
|
||||
Key::Char('c') => {
|
||||
clock.color_index = None;
|
||||
layout.force_redraw = true;
|
||||
},
|
||||
// Delete last alarm on 'd'.
|
||||
Key::Char('d') => {
|
||||
if alarm_roster.pop().is_some() {
|
||||
// If we remove the last alarm we have to reset "countdown"
|
||||
// manually. It is safe to do it anyway.
|
||||
countdown.reset();
|
||||
layout.force_redraw = true;
|
||||
}
|
||||
},
|
||||
// Exit on q and ^C.
|
||||
Key::Char('q') | Key::Ctrl('c') => break,
|
||||
// Force redraw on ^R.
|
||||
Key::Ctrl('r') => layout.force_redraw = true,
|
||||
// Suspend an ^Z.
|
||||
Key::Ctrl('z') => {
|
||||
suspend(&mut stdout);
|
||||
layout.force_redraw = true;
|
||||
},
|
||||
// Enter.
|
||||
Key::Char('\n') => {
|
||||
if buffer.len() > 0 {
|
||||
if let Err(e) = alarm_roster.add(&buffer) {
|
||||
// Error while processing input buffer.
|
||||
error_msg(&mut stdout, &layout, e);
|
||||
} else {
|
||||
// Input buffer processed without error.
|
||||
layout.force_redraw = true;
|
||||
}
|
||||
buffer.clear();
|
||||
}
|
||||
},
|
||||
// Escape ^W, and ^U clear input buffer.
|
||||
Key::Esc | Key::Ctrl('w') | Key::Ctrl('u') => {
|
||||
buffer.clear();
|
||||
buffer_updated = true;
|
||||
},
|
||||
// Backspace.
|
||||
Key::Backspace => {
|
||||
// Delete last char in buffer. It makes no difference to us
|
||||
// if this succeeds of fails.
|
||||
let _ = buffer.pop();
|
||||
buffer_updated = true;
|
||||
},
|
||||
Key::Char(c) => {
|
||||
if c.is_ascii_digit() {
|
||||
buffer.push(c);
|
||||
buffer_updated = true;
|
||||
} else if buffer.len() > 0 && c == ':' {
|
||||
buffer.push(':');
|
||||
buffer_updated = true;
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Update input buffer display.
|
||||
if buffer_updated {
|
||||
draw_buffer(&mut stdout, &layout, &buffer);
|
||||
buffer_updated = false;
|
||||
stdout.flush().unwrap();
|
||||
}
|
||||
|
||||
let elapsed = if clock.paused {
|
||||
clock.elapsed
|
||||
} else {
|
||||
// Should never owerflow as we reestablish a new "start"
|
||||
// instant every 24 hours.
|
||||
clock.start.elapsed().as_secs() as u32
|
||||
};
|
||||
|
||||
// Update screen content if necessary.
|
||||
if elapsed != clock.elapsed || layout.force_redraw {
|
||||
// Update clock. Advance one day after 24 hours.
|
||||
if elapsed < 24 * 60 * 60 {
|
||||
clock.elapsed = elapsed;
|
||||
} else {
|
||||
clock.next_day();
|
||||
// "clock.elapsed" set by "clock.next_day()".
|
||||
alarm_roster.reset_all();
|
||||
layout.force_recalc = true;
|
||||
}
|
||||
|
||||
// Force recalculation of layout if we start displaying hours.
|
||||
if clock.elapsed == 3600 { layout.force_recalc = true };
|
||||
// Update screen size information and calculate the clock position.
|
||||
layout.update(clock.elapsed >= 3600);
|
||||
|
||||
// Check for exceeded alarms.
|
||||
if alarm_roster.check(&mut clock, &layout, &mut countdown) {
|
||||
// Write ASCII bell code.
|
||||
write!(stdout, "{}", 0x07 as char).unwrap();
|
||||
layout.force_redraw = true;
|
||||
|
||||
if config.alarm_exec.len() > 0 {
|
||||
alarm_exec(&config, clock.elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the screen and redraw menu bar, alarm roster and buffer if
|
||||
// requested.
|
||||
if layout.force_redraw {
|
||||
write!(stdout,
|
||||
"{}{}{}{}{}",
|
||||
clear::All,
|
||||
cursor::Goto(1, 1),
|
||||
style::Faint,
|
||||
// Use a compressed version of the menu bar if necessary.
|
||||
if layout.width >= MENUBAR.len() as u16 {
|
||||
MENUBAR
|
||||
} else if layout.width >= MENUBAR_SHORT.len() as u16 {
|
||||
MENUBAR_SHORT
|
||||
} else {
|
||||
""
|
||||
},
|
||||
style::Reset,)
|
||||
.unwrap();
|
||||
|
||||
// Redraw list of alarms.
|
||||
alarm_roster.draw(&mut stdout, &mut layout);
|
||||
|
||||
// Redraw buffer.
|
||||
draw_buffer(&mut stdout, &layout, &buffer);
|
||||
}
|
||||
|
||||
clock.draw(&mut stdout, &layout);
|
||||
|
||||
// Display countdown.
|
||||
if countdown.value > 0 {
|
||||
countdown.draw(&mut stdout);
|
||||
}
|
||||
|
||||
// Reset redraw_all and flush stdout.
|
||||
layout.force_redraw = false;
|
||||
stdout.flush().unwrap();
|
||||
}
|
||||
|
||||
// Main loop delay.
|
||||
thread::sleep(time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
// Main loop exited. Clear screen and restore cursor.
|
||||
write!(stdout,
|
||||
"{}{}{}",
|
||||
clear::BeforeCursor,
|
||||
cursor::Goto(1, 1),
|
||||
cursor::Show)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn usage() {
|
||||
println!("{}\n{}", NAME, USAGE);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
// Parse command line arguments into "config".
|
||||
fn parse_args(config: &mut Config) {
|
||||
for arg in env::args().skip(1) {
|
||||
match arg.as_str() {
|
||||
"-h" | "--help" => usage(),
|
||||
"-v" | "--version" => {
|
||||
println!("{} {}", NAME, VERSION);
|
||||
std::process::exit(0);
|
||||
}
|
||||
"-p" | "--plain" => { config.plain = true; },
|
||||
"-e" | "--exec" => {
|
||||
// Find position of this flag.
|
||||
let i = env::args().position(|s| { s == "-e" || s == "--exec" }).unwrap();
|
||||
// Copy everything thereafter.
|
||||
config.alarm_exec = env::args().skip(i + 1).collect();
|
||||
if config.alarm_exec.len() == 0 {
|
||||
usage();
|
||||
} else {
|
||||
// Ignore everything after this flag.
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => usage(), // Unrecognized flag.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Suspend execution by raising SIGTSTP.
|
||||
fn suspend<W: Write>(stdout: &mut RawTerminal<W>) {
|
||||
write!(stdout,
|
||||
"{}{}{}",
|
||||
cursor::Goto(1,1),
|
||||
clear::All,
|
||||
cursor::Show)
|
||||
.unwrap();
|
||||
stdout.flush().unwrap();
|
||||
stdout.suspend_raw_mode()
|
||||
.unwrap_or_else(|error| {
|
||||
eprintln!("Failed to leave raw terminal mode prior to suspend: {}", error);
|
||||
});
|
||||
|
||||
let result = unsafe { libc::raise(libc::SIGTSTP) };
|
||||
if result != 0 {
|
||||
panic!("{}", std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
stdout.activate_raw_mode()
|
||||
.unwrap_or_else(|error| {
|
||||
eprintln!("Failed to re-enter raw terminal mode after suspend: {}", error);
|
||||
std::process::exit(1);
|
||||
});
|
||||
write!(stdout,
|
||||
"{}{}",
|
||||
clear::All,
|
||||
cursor::Hide)
|
||||
.unwrap_or_else(|error| {
|
||||
eprintln!("Error writing to stdout: {}", error);
|
||||
std::process::exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
// Draw input buffer.
|
||||
fn draw_buffer<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, buffer: &String) {
|
||||
if buffer.len() > 0 {
|
||||
write!(stdout,
|
||||
"{}{}Add alarm: {}",
|
||||
cursor::Goto(layout.buffer.col, layout.buffer.line),
|
||||
clear::CurrentLine,
|
||||
buffer)
|
||||
.unwrap();
|
||||
} else {
|
||||
// Clear buffer display.
|
||||
write!(stdout,
|
||||
"{}{}",
|
||||
cursor::Goto(layout.buffer.col, layout.buffer.line),
|
||||
clear::CurrentLine)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Print error message.
|
||||
fn error_msg<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, msg: &'static str) {
|
||||
write!(stdout,
|
||||
"{}{}{}{}",
|
||||
cursor::Goto(layout.error.col, layout.error.line),
|
||||
color::Fg(color::LightRed),
|
||||
msg,
|
||||
color::Fg(color::Reset))
|
||||
.unwrap();
|
||||
}
|
||||
|
Loading…
Reference in a new issue