Initial commit.

This commit is contained in:
shy 2021-04-05 12:20:24 +02:00
commit c04bde3ba4
8 changed files with 1182 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
\.*.swp

57
Cargo.lock generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}