This commit is contained in:
Shy 2021-04-17 18:20:39 +02:00
parent b10886508c
commit 35a8739f27
10 changed files with 511 additions and 493 deletions

View file

@ -1,20 +1,19 @@
use std::io::Write;
use std::process::{Command, Stdio, Child};
use std::io::BufRead;
use termion::{color, cursor, style};
use termion::raw::RawTerminal;
use unicode_width::UnicodeWidthStr;
use crate::Config;
use crate::clock::Clock;
use crate::consts::{COLOR, LABEL_SIZE_LIMIT};
use crate::layout::{Layout, Position};
use crate::utils::*;
use crate::consts::{COLOR, LABEL_SIZE_LIMIT};
use crate::Config;
use std::io::BufRead;
use std::io::Write;
use std::process::{Child, Command, Stdio};
use termion::raw::RawTerminal;
use termion::{color, cursor, style};
use unicode_width::UnicodeWidthStr;
// Delimiter between time and label. Remember to update usage information in
// consts.rs when changing this.
const DELIMITER: char = '/';
pub struct Countdown {
pub value: u32,
position: Option<Position>,
@ -42,17 +41,17 @@ impl Countdown {
}
// Draw countdown.
pub fn draw<W: Write>(&self, stdout: &mut RawTerminal<W>)
-> Result<(), std::io::Error>
{
pub fn draw<W: Write>(&self, stdout: &mut RawTerminal<W>) -> Result<(), std::io::Error> {
if let Some(pos) = &self.position {
if self.value < 3600 {
// Show minutes and seconds.
write!(stdout,
write!(
stdout,
"{}(-{:02}:{:02})",
cursor::Goto(pos.col, pos.line),
(self.value / 60) % 60,
self.value % 60)?;
self.value % 60
)?;
if self.value == 3599 {
// Write three additional spaces after switching from hour display to
// minute display.
@ -60,29 +59,22 @@ impl Countdown {
}
} else {
// Show hours, minutes and seconds.
write!(stdout,
write!(
stdout,
"{}(-{:02}:{:02}:{:02})",
cursor::Goto(pos.col, pos.line),
self.value / 3600,
(self.value / 60) % 60,
self.value % 60)?;
self.value % 60
)?;
}
}
Ok(())
}
pub fn place(
&mut self,
layout: &Layout,
alarm: &Alarm,
offset: usize,
index: usize,
) {
pub fn place(&mut self, layout: &Layout, alarm: &Alarm, offset: usize, index: usize) {
// Compute position.
let mut col =
layout.roster.col
+ 3
+ UnicodeWidthStr::width(alarm.label.as_str()) as u16;
let mut col = layout.roster.col + 3 + UnicodeWidthStr::width(alarm.label.as_str()) as u16;
let mut line = layout.roster.line + index as u16;
// Compensate for "hidden" items in the alarm roster.
@ -176,8 +168,12 @@ impl AlarmRoster {
}
// Skip if time is out of boundaries.
if time == 0 { return Err("Evaluates to zero.") };
if time >= 24 * 60 * 60 { return Err("Values >24h not supported.") };
if time == 0 {
return Err("Evaluates to zero.");
};
if time >= 24 * 60 * 60 {
return Err("Values >24h not supported.");
};
// Filter out double entries.
if self.list.iter().any(|a| a.time == time) {
return Err("Already exists.");
@ -208,7 +204,11 @@ impl AlarmRoster {
// Offset ceiling according to layout information.
fn adjust_offset(&mut self, layout: &Layout) {
self.offset = self.offset.min(self.list.len().saturating_sub(layout.roster_height as usize));
self.offset = self.offset.min(
self.list
.len()
.saturating_sub(layout.roster_height as usize),
);
}
// Check for active alarms.
@ -217,30 +217,38 @@ impl AlarmRoster {
}
pub fn scroll_up(&mut self, layout: &Layout) {
let excess = self.list.len().saturating_sub(layout.roster_height as usize);
let excess = self
.list
.len()
.saturating_sub(layout.roster_height as usize);
self.offset = excess.min(self.offset.saturating_sub(1));
}
pub fn scroll_down(&mut self, layout: &Layout) {
let excess = self.list.len().saturating_sub(layout.roster_height as usize);
let excess = self
.list
.len()
.saturating_sub(layout.roster_height as usize);
self.offset = excess.min(self.offset.saturating_add(1));
}
// Find and process exceeded alarms.
pub fn check(&mut self,
pub fn check(
&mut self,
clock: &mut Clock,
layout: &Layout,
countdown: &mut Countdown,
force_redraw: bool,
) -> Option<&Alarm>
{
) -> Option<&Alarm> {
let mut ret = None;
for (index, alarm) in self.list.iter_mut()
for (index, alarm) in self
.list
.iter_mut()
.enumerate()
// Ignore alarms marked exceeded.
.filter(|(_, a)| !a.exceeded) {
.filter(|(_, a)| !a.exceeded)
{
if alarm.time <= clock.elapsed {
// Found alarm to raise.
alarm.exceeded = true;
@ -267,35 +275,41 @@ impl AlarmRoster {
stdout: &mut RawTerminal<W>,
layout: &mut Layout,
config: &Config,
) -> Result<(), std::io::Error>
{
) -> Result<(), std::io::Error> {
// Match offset to layout.
self.adjust_offset(&layout);
for (i, alarm) in self.list.iter()
.skip(self.offset)
.enumerate()
{
for (i, alarm) in self.list.iter().skip(self.offset).enumerate() {
// Add 1 to compensate for the line "[...]".
let line = layout.roster.line + i as u16;
if self.offset > 0 && i == 0 {
// Indicate hidden items at top.
write!(stdout,
write!(
stdout,
"{}{}{}{}",
cursor::Goto(layout.roster.col, line),
style::Faint,
if config.fancy { "╶╴▲╶╴" } else { "[ ^ ]" },
if config.fancy {
"╶╴▲╶╴"
} else {
"[ ^ ]"
},
style::Reset,
)?;
continue;
} else if i == layout.roster_height as usize {
// Indicate hidden items at bottom.
write!(stdout,
write!(
stdout,
"{}{}{}{}",
cursor::Goto(layout.roster.col, line),
style::Faint,
if config.fancy { "╶╴▼╶╴" } else { "[ v ]" },
if config.fancy {
"╶╴▼╶╴"
} else {
"[ v ]"
},
style::Reset,
)?;
break;
@ -303,7 +317,8 @@ impl AlarmRoster {
match alarm.exceeded {
true if config.fancy => {
write!(stdout,
write!(
stdout,
"{}{}{}{} {} {}🭬{}{}",
cursor::Goto(layout.roster.col, line),
color::Fg(COLOR[alarm.color_index]),
@ -314,18 +329,20 @@ impl AlarmRoster {
color::Fg(color::Reset),
style::Reset,
)?;
},
}
false if config.fancy => {
write!(stdout,
write!(
stdout,
"{}{}█🭬{}{}",
cursor::Goto(layout.roster.col, line),
color::Fg(COLOR[alarm.color_index]),
color::Fg(color::Reset),
&alarm.label,
)?;
},
}
true => {
write!(stdout,
write!(
stdout,
"{}{}{}{} {} {}{}",
cursor::Goto(layout.roster.col, line),
color::Fg(COLOR[alarm.color_index]),
@ -335,16 +352,17 @@ impl AlarmRoster {
color::Fg(color::Reset),
style::Reset,
)?;
},
}
false => {
write!(stdout,
write!(
stdout,
"{}{} {} {}",
cursor::Goto(layout.roster.col, line),
color::Bg(COLOR[alarm.color_index]),
color::Bg(color::Reset),
&alarm.label,
)?;
},
}
}
}
Ok(())
@ -355,10 +373,16 @@ impl AlarmRoster {
let mut width: u16 = 0;
for alarm in &self.list {
let length = UnicodeWidthStr::width(alarm.label.as_str()) as u16;
if length > width { width = length };
if length > width {
width = length
};
}
// Actual width is 4 columns wider if it's not 0.
if width == 0 { 0 } else { width.saturating_add(4) }
if width == 0 {
0
} else {
width.saturating_add(4)
}
}
// Reset every alarm.
@ -383,19 +407,14 @@ impl AlarmRoster {
}
// Read alarm times from stdin.
pub fn from_stdin(&mut self, stdin: std::io::Stdin)
-> Result<(), String>
{
pub fn from_stdin(&mut self, stdin: std::io::Stdin) -> Result<(), String> {
for line in stdin.lock().lines() {
match line {
Ok(line)
if !line.starts_with('#')
&& !line.trim().is_empty()
=> {
Ok(line) if !line.starts_with('#') && !line.trim().is_empty() => {
if let Err(e) = self.add(&line) {
return Err(format!("Value \"{}\": {}", line, e));
}
},
}
Ok(_) => (), // Discard comments and empty lines.
Err(e) => return Err(e.to_string()),
}
@ -409,7 +428,12 @@ pub fn exec_command(command: &Vec<String>, elapsed: u32, label: &String) -> Opti
let time = if elapsed < 3600 {
format!("{:02}:{:02}", elapsed / 60, elapsed % 60)
} else {
format!("{:02}:{:02}:{:02}", elapsed /3600, (elapsed / 60) % 60, elapsed % 60)
format!(
"{:02}:{:02}:{:02}",
elapsed / 3600,
(elapsed / 60) % 60,
elapsed % 60
)
};
let mut args: Vec<String> = Vec::new();
@ -423,7 +447,8 @@ pub fn exec_command(command: &Vec<String>, elapsed: u32, label: &String) -> Opti
.args(args)
.stdout(Stdio::null())
.stdin(Stdio::null())
.spawn() {
.spawn()
{
Ok(child) => Some(child),
Err(error) => {
eprintln!("Error: Could not execute command. ({})", error);
@ -431,4 +456,3 @@ pub fn exec_command(command: &Vec<String>, elapsed: u32, label: &String) -> Opti
}
}
}

View file

@ -1,15 +1,14 @@
extern crate unicode_segmentation;
use std::io::Write;
use termion::{clear, cursor, color};
use termion::raw::RawTerminal;
use crate::layout::Layout;
use unicode_width::UnicodeWidthStr;
use std::io::Write;
use termion::raw::RawTerminal;
use termion::{clear, color, cursor};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
const PROMPT: &str = "Add alarm: ";
// Input buffer.
pub struct Buffer {
content: String,
@ -50,8 +49,7 @@ impl Buffer {
pub fn strip_word(&mut self) {
// Reset error message.
self.message = None;
let iter = UnicodeSegmentation::split_word_bound_indices(
self.content.as_str().trim_end());
let iter = UnicodeSegmentation::split_word_bound_indices(self.content.as_str().trim_end());
if let Some((index, _)) = iter.last() {
self.content.truncate(index);
@ -78,54 +76,55 @@ impl Buffer {
&mut self,
stdout: &mut RawTerminal<W>,
layout: &mut Layout,
) -> Result<(), std::io::Error>
{
) -> Result<(), std::io::Error> {
// Write error message if present and return.
if let Some(msg) = self.message {
write!(stdout,
write!(
stdout,
"{}{}{}{}{}{}{}",
cursor::Hide,
cursor::Goto( layout.buffer.col, layout.buffer.line),
cursor::Goto(layout.buffer.col, layout.buffer.line),
clear::CurrentLine,
PROMPT,
color::Fg(color::LightRed),
&msg,
color::Fg(color::Reset))?;
color::Fg(color::Reset)
)?;
return Ok(());
}
if self.content.is_empty() {
// Clear buffer display.
write!(stdout,
write!(
stdout,
"{}{}{}",
cursor::Goto(layout.buffer.col, layout.buffer.line),
clear::CurrentLine,
cursor::Hide)?;
cursor::Hide
)?;
} else {
// Check if buffer exceeds limits.
while UnicodeWidthStr::width(self.content.as_str())
+ UnicodeWidthStr::width(PROMPT)
while UnicodeWidthStr::width(self.content.as_str()) + UnicodeWidthStr::width(PROMPT)
> layout.width as usize
{
self.content.pop();
}
write!(stdout,
write!(
stdout,
"{}{}{}{}{}",
cursor::Goto(layout.buffer.col, layout.buffer.line),
clear::CurrentLine,
PROMPT,
cursor::Show,
&self.content)?;
&self.content
)?;
}
Ok(())
}
// Draw error message at input buffer position.
pub fn message(
&mut self,
msg: &'static str,
) {
pub fn message(&mut self, msg: &'static str) {
self.message = Some(msg);
}
}

View file

@ -1,12 +1,12 @@
pub mod font;
use std::time;
use std::io::Write;
use termion::{color, cursor, style};
use termion::raw::RawTerminal;
use crate::consts::COLOR;
use crate::Config;
use crate::layout::{Layout, Position};
use crate::Config;
use std::io::Write;
use std::time;
use termion::raw::RawTerminal;
use termion::{color, cursor, style};
enum Pause {
Instant(time::Instant),
@ -60,10 +60,10 @@ impl Clock {
if let Some(start) = self.start.checked_add(delay.elapsed()) {
self.start = start;
}
},
}
Pause::Time((secs, _days)) => {
self.start = time::Instant::now() - time::Duration::from_secs(secs as u64);
},
}
Pause::None => (), // O_o
}
@ -126,8 +126,7 @@ impl Clock {
mut stdout: &mut RawTerminal<W>,
layout: &Layout,
force_redraw: bool,
) -> Result<(), std::io::Error>
{
) -> Result<(), std::io::Error> {
// Setup style and color if appropriate.
if self.paused {
write!(stdout, "{}", style::Faint)?;
@ -140,17 +139,10 @@ impl Clock {
if force_redraw || self.elapsed % 3600 == 0 {
// Draw hours if necessary.
if self.elapsed >= 3600 {
self.draw_digit_pair(
&mut stdout,
self.elapsed / 3600,
&layout.clock_hr,
)?;
self.draw_digit_pair(&mut stdout, self.elapsed / 3600, &layout.clock_hr)?;
// Draw colon.
self.draw_colon(
&mut stdout,
&layout.clock_colon1,
)?;
self.draw_colon(&mut stdout, &layout.clock_colon1)?;
}
// Draw days.
@ -161,12 +153,10 @@ impl Clock {
if self.days == 1 { "DAY" } else { "DAYS" },
);
write!(stdout,
write!(
stdout,
"{}{:>11}",
cursor::Goto(
layout.clock_days.col,
layout.clock_days.line,
),
cursor::Goto(layout.clock_days.col, layout.clock_days.line,),
day_count,
)?;
}
@ -174,35 +164,20 @@ impl Clock {
// Draw minutes if necessary. Once every minute or on request.
if force_redraw || self.elapsed % 60 == 0 {
self.draw_digit_pair(
&mut stdout,
(self.elapsed % 3600) / 60,
&layout.clock_min,
)?;
self.draw_digit_pair(&mut stdout, (self.elapsed % 3600) / 60, &layout.clock_min)?;
}
// Draw colon if necessary.
if force_redraw {
self.draw_colon(
&mut stdout,
&layout.clock_colon0,
)?;
self.draw_colon(&mut stdout, &layout.clock_colon0)?;
}
// Draw seconds.
self.draw_digit_pair(
&mut stdout,
self.elapsed % 60,
&layout.clock_sec,
)?;
self.draw_digit_pair(&mut stdout, self.elapsed % 60, &layout.clock_sec)?;
// Reset color and style.
if self.paused || self.color_index != None {
write!(stdout,
"{}{}",
style::NoFaint,
color::Fg(color::Reset),
)?;
write!(stdout, "{}{}", style::NoFaint, color::Fg(color::Reset),)?;
}
Ok(())
}
@ -212,13 +187,13 @@ impl Clock {
stdout: &mut RawTerminal<W>,
value: u32,
pos: &Position,
) -> Result<(), std::io::Error>
{
) -> Result<(), std::io::Error> {
let left = self.font.digits[value as usize / 10].iter();
let right = self.font.digits[value as usize % 10].iter();
for (i, (left, right)) in left.zip(right).enumerate() {
write!(stdout,
write!(
stdout,
"{}{} {}",
cursor::Goto(pos.col, pos.line + i as u16),
left,
@ -233,9 +208,9 @@ impl Clock {
&self,
stdout: &mut RawTerminal<W>,
pos: &Position,
) -> Result<(), std::io::Error>
{
write!(stdout,
) -> Result<(), std::io::Error> {
write!(
stdout,
"{}{}{}{}",
cursor::Goto(pos.col, pos.line + 1),
self.font.dots.0,
@ -245,4 +220,3 @@ impl Clock {
Ok(())
}
}

View file

@ -1,4 +1,3 @@
const DIGIT_HEIGHT: u16 = 5;
pub struct Font {
@ -12,230 +11,262 @@ pub const NORMAL: Font = Font {
height: DIGIT_HEIGHT,
width: 5,
dots: ('■', '■'),
digits: [[
digits: [
[
// 0
"█▀▀▀█",
"█ █",
"█ █",
"█ █",
"█▄▄▄█",
], [
],
[
// 1
" ▀█ ",
"",
"",
"",
""
], [
"",
],
[
// 2
"▀▀▀▀█",
"",
"█▀▀▀▀",
"",
"█▄▄▄▄"
], [
"█▄▄▄▄",
],
[
// 3
"▀▀▀▀█",
"",
" ▀▀▀█",
"",
"▄▄▄▄█"
], [
"▄▄▄▄█",
],
[
// 4
"",
"█ █ ",
"▀▀▀█▀",
"",
""
], [
"",
],
[
// 5
"█▀▀▀▀",
"",
"▀▀▀▀█",
"",
"▄▄▄▄█"
], [
"▄▄▄▄█",
],
[
// 6
"",
"",
"█▀▀▀█",
"█ █",
"█▄▄▄█"
], [
"█▄▄▄█",
],
[
// 7
"▀▀▀▀█",
"",
"",
"",
"",
], [
],
[
// 8
"█▀▀▀█",
"█ █",
"█▀▀▀█",
"█ █",
"█▄▄▄█"
], [
"█▄▄▄█",
],
[
// 9
"█▀▀▀█",
"█ █",
"▀▀▀▀█",
"",
""
]],
"",
],
],
};
pub const PLAIN: Font = Font {
height: DIGIT_HEIGHT,
width: 5,
dots: ('█', '█'),
digits: [[
digits: [
[
// 0
"█████",
"█ █",
"█ █",
"█ █",
"█████"
], [
"█████",
],
[
// 1
" ██ ",
"",
"",
"",
""
], [
"",
],
[
// 2
"█████",
"",
"█████",
"",
"█████"
], [
"█████",
],
[
// 3
"█████",
"",
" ████",
"",
"█████"
], [
"█████",
],
[
// 4
"",
"█ █ ",
"█████",
"",
""
], [
"",
],
[
// 5
"█████",
"",
"█████",
"",
"█████"
], [
"█████",
],
[
// 6
"",
"",
"█████",
"█ █",
"█████"
], [
"█████",
],
[
// 7
"█████",
"",
"",
"",
""
], [
"",
],
[
// 8
"█████",
"█ █",
"█████",
"█ █",
"█████"
], [
"█████",
],
[
// 9
"█████",
"█ █",
"█████",
"",
""
]],
"",
],
],
};
pub const CHROME: Font = Font {
height: DIGIT_HEIGHT,
width: 5,
dots: ('▄', '🮏'),
digits: [[
digits: [
[
// 0
"█▀▀▀█",
"█ █",
"▀ ▀",
"🮐 🮐",
"🮐🮏🮏🮏🮐",
], [
],
[
// 1
" ▀█ ",
"",
"",
" 🮐 ",
" 🮐 "
], [
" 🮐 ",
],
[
// 2
"▀▀▀▀█",
"",
"▀▀▀▀▀",
"🮐 ",
"🮐🮏🮏🮏🮏"
], [
"🮐🮏🮏🮏🮏",
],
[
// 3
"▀▀▀▀█",
"",
" ▀▀▀▀",
" 🮐",
"🮏🮏🮏🮏🮐"
], [
"🮏🮏🮏🮏🮐",
],
[
// 4
"",
"█ █ ",
"▀▀▀▀▀",
" 🮐 ",
" 🮐 "
], [
" 🮐 ",
],
[
// 5
"█▀▀▀▀",
"",
"▀▀▀▀▀",
" 🮐",
"🮏🮏🮏🮏🮐"
], [
"🮏🮏🮏🮏🮐",
],
[
// 6
"",
"",
"▀▀▀▀▀",
"🮐 🮐",
"🮐🮏🮏🮏🮐"
], [
"🮐🮏🮏🮏🮐",
],
[
// 7
"▀▀▀▀█",
"",
"",
" 🮐 ",
" 🮐 ",
], [
],
[
// 8
"█▀▀▀█",
"█ █",
"▀▀▀▀▀",
"🮐 🮐",
"🮐🮏🮏🮏🮐"
], [
"🮐🮏🮏🮏🮐",
],
[
// 9
"█▀▀▀█",
"█ █",
"▀▀▀▀▀",
" 🮐",
" 🮐"
]],
" 🮐",
],
],
};

View file

@ -1,4 +1,3 @@
pub const COLOR: [&dyn termion::color::Color; 6] = [
&termion::color::LightGreen,
&termion::color::LightYellow,
@ -14,8 +13,10 @@ pub const LABEL_SIZE_LIMIT: usize = 32;
pub mod ui {
pub const NAME: &str = env!("CARGO_PKG_NAME");
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const USAGE: &str = concat!("USAGE: ", env!("CARGO_PKG_NAME"),
" [-h|-v] [-e|--exec COMMAND] [-p] [-q] [ALARM[/LABEL]]
pub const USAGE: &str = concat!(
"USAGE: ",
env!("CARGO_PKG_NAME"),
" [-h|-v] [-e|--exec COMMAND] [-p] [-q] [ALARM[/LABEL]]
PARAMETERS:
[ALARM TIME[/LABEL]] Any number of alarm times (HH:MM:SS) with optional
@ -32,14 +33,13 @@ OPTIONS:
-q, --quit Quit program after last alarm.
SIGNALS: <SIGUSR1> Reset clock.
<SIGUSR2> Pause or un-pause clock.");
<SIGUSR2> Pause or un-pause clock."
);
pub const MENUBAR: &str =
"[0-9] Add alarm [d] Delete alarm [SPACE] Pause [r] Reset [c] Clear color [q] Quit";
pub const MENUBAR_SHORT: &str =
"[0-9] Add [d] Delete [SPACE] Pause [r] Reset [c] Clear [q] Quit";
pub const MENUBAR_INS: &str =
"Format: HH:MM:SS/LABEL [ENTER] Accept [ESC] Cancel [CTR-C] Quit";
pub const MENUBAR_PAUSED: &str =
"[SPACE] Continue [r] Reset [UP]/[DOWN] Set clock";
pub const MENUBAR_PAUSED: &str = "[SPACE] Continue [r] Reset [UP]/[DOWN] Set clock";
}

View file

@ -8,7 +8,7 @@ pub struct Position {
impl Position {
// Terminal positions are 1-based.
pub fn new() -> Position {
Position {col: 1, line: 1}
Position { col: 1, line: 1 }
}
}
@ -48,7 +48,7 @@ impl Layout {
clock_colon1: Position::new(),
clock_hr: Position::new(),
clock_days: Position::new(),
roster: Position {col: 1, line: 3},
roster: Position { col: 1, line: 3 },
roster_width: 0,
roster_height: 0,
buffer: Position::new(),
@ -56,9 +56,7 @@ impl Layout {
}
// Update layout. Returns true when changes were made.
pub fn update(&mut self, clock: &Clock, force: bool)
-> Result<bool, std::io::Error>
{
pub fn update(&mut self, clock: &Clock, force: bool) -> Result<bool, std::io::Error> {
if self.force_recalc || force {
self.force_recalc = false;
let (width, height) = termion::terminal_size()?;
@ -79,13 +77,7 @@ impl Layout {
}
#[cfg(test)]
pub fn test_update(
&mut self,
clock: &Clock,
width: u16,
height: u16,
roster_width: u16,
) {
pub fn test_update(&mut self, clock: &Clock, width: u16, height: u16, roster_width: u16) {
self.width = width;
self.height = height;
self.clock_width = clock.get_width();
@ -104,7 +96,9 @@ impl Layout {
// terminal.
fn compute(&mut self, display_hours: bool) {
// Prevent integer overflow at very low screen sizes.
if self.width < self.clock_width || self.height < self.clock_height { return; }
if self.width < self.clock_width || self.height < self.clock_height {
return;
}
let middle: u16 = self.height / 2 - 1;
@ -163,4 +157,3 @@ impl Layout {
}
}
}

View file

@ -1,36 +1,34 @@
extern crate termion;
extern crate signal_hook;
extern crate termion;
pub mod alarm;
mod buffer;
pub mod clock;
pub mod consts;
pub mod layout;
pub mod utils;
#[cfg(test)]
mod tests;
pub mod utils;
use std::{env, process, thread, time};
use std::io::Write;
pub use alarm::AlarmRoster;
use alarm::{exec_command, Countdown};
use buffer::Buffer;
use clock::{font, Clock};
pub use consts::ui::*;
use layout::Layout;
use signal_hook::consts::signal::*;
use signal_hook::iterator::Signals;
use signal_hook::low_level;
use termion::{clear, cursor, style};
use termion::raw::{IntoRawMode, RawTerminal};
use std::io::Write;
use std::{env, process, thread, time};
use termion::event::Key;
use termion::input::TermRead;
use buffer::Buffer;
use clock::{Clock, font};
use layout::Layout;
use alarm::{Countdown, exec_command};
pub use alarm::AlarmRoster;
pub use consts::ui::*;
use termion::raw::{IntoRawMode, RawTerminal};
use termion::{clear, cursor, style};
pub fn run(
config: Config,
mut alarm_roster: AlarmRoster,
) -> Result<Option<process::Child>, std::io::Error>
{
) -> Result<Option<process::Child>, std::io::Error> {
let mut layout = Layout::new();
// Initialise roster_width.
layout.set_roster_width(alarm_roster.width());
@ -47,13 +45,7 @@ pub fn run(
// Register signals.
let mut signals = Signals::new(&[
SIGTSTP,
SIGCONT,
SIGWINCH,
SIGTERM,
SIGINT,
SIGUSR1,
SIGUSR2,
SIGTSTP, SIGCONT, SIGWINCH, SIGTERM, SIGINT, SIGUSR1, SIGUSR2,
])?;
// Main loop entry.
@ -67,7 +59,7 @@ pub fn run(
SIGCONT => {
restore_after_suspend(&mut stdout)?;
force_redraw = true;
},
}
SIGWINCH => layout.schedule_recalc(),
// Exit main loop on SIGTERM and SIGINT.
SIGTERM | SIGINT => break 'outer,
@ -77,12 +69,12 @@ pub fn run(
alarm_roster.reset_all();
layout.schedule_recalc();
force_redraw = true;
},
}
// (Un-)Pause clock on SIGUSR2.
SIGUSR2 => {
clock.toggle();
force_redraw = true;
},
}
// We didn't register anything else.
_ => unreachable!(),
}
@ -127,25 +119,25 @@ pub fn run(
Ok(Some(status)) if status.success() => child = None,
// Abnormal exit.
Ok(Some(status)) => {
eprintln!("Spawned process terminated with non-zero exit status. ({})", status);
eprintln!(
"Spawned process terminated with non-zero exit status. ({})",
status
);
child = None;
},
}
// Process is still running.
Ok(None) => (),
// Other error.
Err(error) => {
eprintln!("Error executing command. ({})", error);
child = None;
},
}
}
}
// Check for exceeded alarms.
if let Some(alarm) = alarm_roster.check(
&mut clock,
&layout,
&mut countdown,
force_redraw)
if let Some(alarm) =
alarm_roster.check(&mut clock, &layout, &mut countdown, force_redraw)
{
// Do not react to exceeded alarms if the clock is paused.
if !clock.paused {
@ -158,13 +150,17 @@ pub fn run(
// Run command if configured and no command is running.
Some(ref command) if child.is_none() => {
child = exec_command(command, alarm.time, &alarm.label);
},
}
// Last command is still running.
Some(_) => eprintln!("Not executing command, as its predecessor is still running"),
Some(_) => {
eprintln!("Not executing command, as its predecessor is still running")
}
None => (),
}
// Quit if configured.
if config.quit && alarm_roster.idle() { break };
if config.quit && alarm_roster.idle() {
break;
};
}
}
@ -172,7 +168,8 @@ pub fn run(
// requested.
if force_redraw {
// Write menu at the top.
write!(stdout,
write!(
stdout,
"{}{}{}{}{}",
cursor::Goto(1, 1),
style::Faint,
@ -189,7 +186,8 @@ pub fn run(
_ => " ",
},
clear::AfterCursor,
style::NoFaint)?;
style::NoFaint
)?;
// Redraw list of alarms.
alarm_roster.draw(&mut stdout, &mut layout, &config)?;
@ -234,13 +232,13 @@ pub fn run(
buffer.visible = false;
force_redraw = true;
}
},
}
// Escape and ^U clear input buffer.
Key::Esc | Key::Ctrl('u') => {
buffer.reset();
buffer.visible = false;
force_redraw = true;
},
}
// ^W removes last word.
Key::Ctrl('w') => {
buffer.strip_word();
@ -248,7 +246,7 @@ pub fn run(
buffer.visible = false;
force_redraw = true;
}
},
}
// Backspace.
Key::Backspace => {
// Delete last char in buffer.
@ -257,7 +255,7 @@ pub fn run(
buffer.visible = false;
force_redraw = true;
}
},
}
// Set clock.
Key::Up if clock.paused => {
clock.shift(10);
@ -267,43 +265,43 @@ pub fn run(
// here.
layout.schedule_recalc();
force_redraw = true;
},
}
Key::Down if clock.paused => {
clock.shift(-10);
alarm_roster.time_travel(&mut clock);
layout.schedule_recalc();
force_redraw = true;
},
}
// Scroll alarm roster.
Key::PageUp => {
alarm_roster.scroll_up(&layout);
force_redraw = true;
},
}
Key::PageDown => {
alarm_roster.scroll_down(&layout);
force_redraw = true;
},
}
// Forward every char if in insert mode.
Key::Char(c) if buffer.visible => {
buffer.push(c);
},
}
// Reset clock on 'r'.
Key::Char('r') => {
clock.reset();
alarm_roster.reset_all();
layout.schedule_recalc();
force_redraw = true;
},
}
// (Un-)Pause on space.
Key::Char(' ') => {
clock.toggle();
force_redraw = true;
},
}
// Clear clock color on 'c'.
Key::Char('c') => {
clock.color_index = None;
force_redraw = true;
},
}
// Delete last alarm on 'd'.
Key::Char('d') => {
if alarm_roster.pop().is_some() {
@ -313,7 +311,7 @@ pub fn run(
countdown.reset();
force_redraw = true;
}
},
}
// Exit on q and ^C.
Key::Char('q') | Key::Ctrl('c') => break,
// Force redraw on ^R.
@ -327,7 +325,7 @@ pub fn run(
force_redraw = true;
// Jump to the start of the main loop.
continue;
},
}
Key::Char(c) => {
if c.is_ascii_digit() {
buffer.push(c);
@ -336,7 +334,7 @@ pub fn run(
} else if !buffer.is_empty() && c == ':' {
buffer.push(':');
}
},
}
// Any other key.
_ => (),
}
@ -347,11 +345,7 @@ pub fn run(
}
// Main loop exited. Clear screen and restore cursor.
write!(stdout,
"{}{}{}",
clear::All,
cursor::Restore,
cursor::Show)?;
write!(stdout, "{}{}{}", clear::All, cursor::Restore, cursor::Show)?;
stdout.flush()?;
Ok(child)
@ -366,9 +360,7 @@ pub struct Config {
impl Config {
// Parse command line arguments into "config".
pub fn new(args: env::Args, alarm_roster: &mut AlarmRoster)
-> Result<Config, String>
{
pub fn new(args: env::Args, alarm_roster: &mut AlarmRoster) -> Result<Config, String> {
let mut config = Config {
quit: false,
fancy: false,
@ -384,16 +376,16 @@ impl Config {
// Print usage information and exit
println!("{}", USAGE);
process::exit(0);
},
}
"-v" | "--version" => {
println!("{} {}", NAME, VERSION);
process::exit(0);
},
}
"-p" | "--plain" => config.font = &font::PLAIN,
"-f" | "--fancy" => {
config.fancy = true;
config.font = &font::CHROME;
},
}
"-q" | "--quit" => config.quit = true,
"-e" | "--exec" => {
if let Some(e) = iter.next() {
@ -401,19 +393,21 @@ impl Config {
} else {
return Err(format!("Missing parameter to \"{}\".", arg));
}
},
}
any if any.starts_with('-') => {
// Unrecognized flag.
return Err(format!("Unrecognized option: \"{}\"\nUse \"-h\" or \"--help\" for a list of valid command line options.", any));
},
}
any => {
// Alarm to add.
if let Err(error) = alarm_roster.add(&String::from(any)) {
return Err(format!("Error adding \"{}\" as alarm. ({})", any, error));
}
},
}
} else { break; } // All command line parameters processed.
}
} else {
break;
} // All command line parameters processed.
}
Ok(config)
}
@ -432,23 +426,27 @@ impl Config {
// Next char is escaped. (If not escaped itself.)
escaped = true;
continue;
},
}
// Keep spaces when escaped or quoted.
' ' if escaped || quoted => { &segment.push(' '); },
' ' if escaped || quoted => {
&segment.push(' ');
}
// Otherwise end the current segment.
' ' => {
if !&segment.is_empty() {
command.push(segment.clone());
&segment.clear();
}
},
}
// Quotation marks toggle quote.
'"' | '\'' if !escaped => quoted = !quoted,
// Carry everything else. Escape if found escaped.
_ => {
if escaped { &segment.push('\\'); }
if escaped {
&segment.push('\\');
}
&segment.push(c);
},
}
}
escaped = false;
}
@ -459,18 +457,20 @@ impl Config {
}
// Prepare to suspend execution. Called on SIGTSTP.
fn suspend<W: Write>(stdout: &mut RawTerminal<W>)
-> Result<(), std::io::Error>
{
write!(stdout,
fn suspend<W: Write>(stdout: &mut RawTerminal<W>) -> Result<(), std::io::Error> {
write!(
stdout,
"{}{}{}",
cursor::Goto(1,1),
cursor::Goto(1, 1),
clear::AfterCursor,
cursor::Show)?;
cursor::Show
)?;
stdout.flush()?;
stdout.suspend_raw_mode()
.unwrap_or_else(|error| {
eprintln!("Failed to leave raw terminal mode prior to suspend: {}", error);
stdout.suspend_raw_mode().unwrap_or_else(|error| {
eprintln!(
"Failed to leave raw terminal mode prior to suspend: {}",
error
);
});
if let Err(error) = low_level::emulate_default_handler(SIGTSTP as i32) {
@ -482,14 +482,13 @@ fn suspend<W: Write>(stdout: &mut RawTerminal<W>)
}
// Set up terminal after SIGTSTP or SIGSTOP.
fn restore_after_suspend<W: Write>(stdout: &mut RawTerminal<W>)
-> Result<(), std::io::Error>
{
stdout.activate_raw_mode()
.unwrap_or_else(|error| {
eprintln!("Failed to re-enter raw terminal mode after suspend: {}", error);
fn restore_after_suspend<W: Write>(stdout: &mut RawTerminal<W>) -> Result<(), std::io::Error> {
stdout.activate_raw_mode().unwrap_or_else(|error| {
eprintln!(
"Failed to re-enter raw terminal mode after suspend: {}",
error
);
process::exit(1);
});
Ok(())
}

View file

@ -1,13 +1,11 @@
use kitchentimer::{run, AlarmRoster, Config};
use std::{env, process};
use kitchentimer::{Config, AlarmRoster, run};
fn main() {
let args = env::args();
let mut alarm_roster = AlarmRoster::new();
// Parse command line arguments into config and alarm roster.
let config = Config::new(args, &mut alarm_roster)
.unwrap_or_else(|e| {
let config = Config::new(args, &mut alarm_roster).unwrap_or_else(|e| {
eprintln!("{}", e);
process::exit(1);
});
@ -27,12 +25,15 @@ fn main() {
Err(error) => {
eprintln!("Main loop exited with error: {}", error);
process::exit(1);
},
}
};
// Wait for remaining spawned process to exit.
if let Some(mut child) = child {
eprint!("Waiting for spawned process (PID {}) to finish ...", child.id());
eprint!(
"Waiting for spawned process (PID {}) to finish ...",
child.id()
);
match child.wait() {
Ok(status) if status.success() => eprintln!(" ok"),
@ -43,5 +44,3 @@ fn main() {
}
}
}

View file

@ -1,5 +1,5 @@
use crate::layout::Layout;
use crate::clock::Clock;
use crate::layout::Layout;
use crate::Config;
fn default_config() -> Config {

View file

@ -6,8 +6,7 @@ pub fn grapheme_truncate(input: &mut String, limit: usize, ellipse: char) {
Some((i, _)) => {
input.truncate(i);
input.push(ellipse);
},
}
None => (),
}
}