Added alarm labels.

This commit is contained in:
shy 2021-04-09 16:43:46 +02:00
parent 320453474c
commit 299ef7fbe6
6 changed files with 155 additions and 75 deletions

11
Cargo.lock generated
View file

@ -12,13 +12,14 @@ version = "0.0.1"
dependencies = [ dependencies = [
"signal-hook", "signal-hook",
"termion", "termion",
"unicode-segmentation",
] ]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.92" version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41"
[[package]] [[package]]
name = "numtoa" name = "numtoa"
@ -74,3 +75,9 @@ dependencies = [
"redox_syscall", "redox_syscall",
"redox_termios", "redox_termios",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"

View file

@ -9,3 +9,4 @@ edition = "2018"
[dependencies] [dependencies]
termion = "1.5.6" termion = "1.5.6"
signal-hook = "0.3.8" signal-hook = "0.3.8"
unicode-segmentation = "1.7.1"

View file

@ -2,8 +2,8 @@ use std::io::Write;
use std::process::{Command, Stdio, Child}; use std::process::{Command, Stdio, Child};
use termion::{color, cursor, style}; use termion::{color, cursor, style};
use termion::raw::RawTerminal; use termion::raw::RawTerminal;
use crate::{Clock, Config, Layout, Position}; use crate::{Clock, Layout, Position};
use crate::common::COLOR; use crate::common::{COLOR, Config, str_length};
pub struct Countdown { pub struct Countdown {
@ -55,7 +55,7 @@ impl Countdown {
pub struct Alarm { pub struct Alarm {
time: u32, time: u32,
display: String, label: String,
color_index: usize, color_index: usize,
exceeded: bool, exceeded: bool,
} }
@ -78,15 +78,25 @@ impl AlarmRoster {
} }
// Parse string and add as alarm. // Parse string and add as alarm.
pub fn add(&mut self, buffer: &String) pub fn add(&mut self, input: &String) -> Result<(), &str> {
-> Result<(), &str> {
let mut index = 0; let mut index = 0;
let mut time: u32 = 0; let mut time: u32 = 0;
let mut label: String;
let time_str: &str;
if let Some(i) = input.find('/') {
label = input[(i + 1)..].trim().to_string();
// TODO: Make decision yes/no.
//label.truncate(24);
time_str = &input[..i].trim();
} else {
label = input.clone();
time_str = &input.trim();
}
// Parse input into seconds. // Parse input into seconds.
if buffer.find(':').is_some() { if time_str.contains(':') {
for sub in buffer.rsplit(':') { for sub in time_str.rsplit(':') {
if !sub.is_empty() { if !sub.is_empty() {
match sub.parse::<u32>() { match sub.parse::<u32>() {
// Valid. // Valid.
@ -101,9 +111,9 @@ impl AlarmRoster {
} }
} else { } else {
// Parse as seconds only. // Parse as seconds only.
match buffer.parse::<u32>() { match time_str.parse::<u32>() {
Ok(d) => time = d, Ok(d) => time = d,
Err(_) => return Err("Could not parse as <u32>."), Err(_) => return Err("Could not parse as integer."),
} }
} }
@ -111,11 +121,9 @@ impl AlarmRoster {
if time == 0 { return Err("Evaluates to zero.") }; if time == 0 { return Err("Evaluates to zero.") };
if time >= 24 * 60 * 60 { return Err("Values >24h not supported.") }; if time >= 24 * 60 * 60 { return Err("Values >24h not supported.") };
let mut display = buffer.clone(); label.shrink_to_fit();
display.shrink_to_fit();
let alarm = Alarm { let alarm = Alarm {
display, label,
time, time,
color_index: (self.list.len() % COLOR.len()), color_index: (self.list.len() % COLOR.len()),
exceeded: false, exceeded: false,
@ -181,7 +189,7 @@ impl AlarmRoster {
let mut col = let mut col =
layout.roster.col layout.roster.col
+ 3 + 3
+ alarm.display.len() as u16; + str_length(&alarm.label);
let mut line = layout.roster.line + index; let mut line = layout.roster.line + index;
// Compensate for "hidden" items in the alarm roster. // Compensate for "hidden" items in the alarm roster.
@ -240,7 +248,7 @@ impl AlarmRoster {
color::Bg(COLOR[alarm.color_index]), color::Bg(COLOR[alarm.color_index]),
color::Bg(color::Reset), color::Bg(color::Reset),
style::Bold, style::Bold,
alarm.display, alarm.label,
style::Reset) style::Reset)
.unwrap(); .unwrap();
} else { } else {
@ -249,7 +257,7 @@ impl AlarmRoster {
cursor::Goto(layout.roster.col, layout.roster.line + index), cursor::Goto(layout.roster.col, layout.roster.line + index),
color::Bg(COLOR[alarm.color_index]), color::Bg(COLOR[alarm.color_index]),
color::Bg(color::Reset), color::Bg(color::Reset),
alarm.display) alarm.label)
.unwrap(); .unwrap();
} }
index += 1; index += 1;
@ -260,7 +268,8 @@ impl AlarmRoster {
pub fn width(&self) -> u16 { pub fn width(&self) -> u16 {
let mut width: u16 = 0; let mut width: u16 = 0;
for alarm in &self.list { for alarm in &self.list {
if alarm.display.len() as u16 > width { width = alarm.display.len() as u16; } let length = str_length(&alarm.label);
if length > width { width = length };
} }
// Actual width is 3 columns wider if it's not 0. // Actual width is 3 columns wider if it's not 0.
if width == 0 { 0 } else { width.saturating_add(3) } if width == 0 { 0 } else { width.saturating_add(3) }

View file

@ -1,5 +1,18 @@
use unicode_segmentation::UnicodeSegmentation;
use termion::color; use termion::color;
pub struct Config {
pub plain: bool,
pub quit: bool,
pub command: Option<Vec<String>>,
}
pub fn str_length(input: &str) -> u16 {
let length = UnicodeSegmentation::graphemes(input, true).count();
length as u16
}
pub const COLOR: [&dyn color::Color; 6] = [ pub const COLOR: [&dyn color::Color; 6] = [
&color::Cyan, &color::Cyan,
&color::Magenta, &color::Magenta,

View file

@ -1,6 +1,5 @@
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use crate::Config;
use crate::common::*; use crate::common::*;
// If screen size falls below these values we skip computation of new // If screen size falls below these values we skip computation of new
@ -30,6 +29,7 @@ pub struct Layout {
pub roster_height: u16, pub roster_height: u16,
pub buffer: Position, pub buffer: Position,
pub error: Position, pub error: Position,
pub cursor: Position,
} }
impl Layout { impl Layout {
@ -52,6 +52,7 @@ impl Layout {
roster_height: 0, roster_height: 0,
buffer: Position {col: 0, line: 0}, buffer: Position {col: 0, line: 0},
error: Position {col: 0, line: 0}, error: Position {col: 0, line: 0},
cursor: Position {col: 1, line: 1},
} }
} }
@ -80,6 +81,11 @@ impl Layout {
self.compute(hours); self.compute(hours);
} }
pub fn can_hold(&self, other: &str) -> bool {
// Only valid for ascii strings.
self.width >= other.len() as u16
}
// Compute the position of various elements based on the size of the // Compute the position of various elements based on the size of the
// terminal. // terminal.
fn compute(&mut self, display_hours: bool) { fn compute(&mut self, display_hours: bool) {
@ -140,6 +146,9 @@ impl Layout {
line: self.height, line: self.height,
col: 12, col: 12,
}; };
// Cursor. Column will be set by main loop.
self.cursor.line = self.buffer.line;
} }
pub fn set_roster_width(&mut self, width: u16) { pub fn set_roster_width(&mut self, width: u16) {

View file

@ -1,5 +1,6 @@
extern crate termion; extern crate termion;
extern crate signal_hook; extern crate signal_hook;
extern crate unicode_segmentation;
mod alarm; mod alarm;
mod clock; mod clock;
mod common; mod common;
@ -19,6 +20,7 @@ use termion::input::TermRead;
use clock::Clock; use clock::Clock;
use alarm::{Countdown, AlarmRoster, exec_command}; use alarm::{Countdown, AlarmRoster, exec_command};
use layout::{Layout, Position}; use layout::{Layout, Position};
use common::{Config, str_length};
const NAME: &str = env!("CARGO_PKG_NAME"); const NAME: &str = env!("CARGO_PKG_NAME");
@ -44,6 +46,8 @@ const MENUBAR: &str =
"[0-9] Add alarm [d] Delete alarm [SPACE] Pause [r] Reset [c] Clear color [q] Quit"; "[0-9] Add alarm [d] Delete alarm [SPACE] Pause [r] Reset [c] Clear color [q] Quit";
const MENUBAR_SHORT: &str = const MENUBAR_SHORT: &str =
"[0-9] Add [d] Delete [SPACE] Pause [r] Reset [c] Clear [q] Quit"; "[0-9] Add [d] Delete [SPACE] Pause [r] Reset [c] Clear [q] Quit";
const MENUBAR_INS: &str =
"Format: HH:MM:SS/LABEL [ENTER] Accept [ESC] Cancel [CTR-C] Quit";
// Needed for signal_hook. // Needed for signal_hook.
const SIGTSTP: usize = signal_hook::consts::SIGTSTP as usize; const SIGTSTP: usize = signal_hook::consts::SIGTSTP as usize;
const SIGWINCH: usize = signal_hook::consts::SIGWINCH as usize; const SIGWINCH: usize = signal_hook::consts::SIGWINCH as usize;
@ -53,12 +57,6 @@ const SIGINT: usize = signal_hook::consts::SIGINT as usize;
const SIGUSR1: usize = signal_hook::consts::SIGUSR1 as usize; const SIGUSR1: usize = signal_hook::consts::SIGUSR1 as usize;
const SIGUSR2: usize = signal_hook::consts::SIGUSR2 as usize; const SIGUSR2: usize = signal_hook::consts::SIGUSR2 as usize;
pub struct Config {
plain: bool,
quit: bool,
command: Option<Vec<String>>,
}
fn main() { fn main() {
let mut config = Config { let mut config = Config {
@ -78,8 +76,11 @@ fn main() {
let mut layout = Layout::new(&config); let mut layout = Layout::new(&config);
let mut clock = Clock::new(); let mut clock = Clock::new();
let mut buffer = String::new(); let mut buffer = String::new();
let mut buffer_updated: bool = false; let mut buffer_updated = false;
let mut countdown = Countdown::new(); let mut countdown = Countdown::new();
// True if in insert mode.
let mut insert_mode = false;
let mut update_menu = true;
// Child process of exec_command(). // Child process of exec_command().
let mut spawned: Option<std::process::Child> = None; let mut spawned: Option<std::process::Child> = None;
@ -140,6 +141,47 @@ fn main() {
// Process input. // Process input.
if let Some(key) = input_keys.next() { if let Some(key) = input_keys.next() {
match key.expect("Error reading input") { match key.expect("Error reading input") {
// Enter.
Key::Char('\n') => {
if !buffer.is_empty() {
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.set_roster_width(alarm_roster.width());
layout.force_redraw = true;
}
buffer.clear();
insert_mode = false;
update_menu = true;
}
},
// Escape ^W, and ^U clear input buffer.
Key::Esc | Key::Ctrl('w') | Key::Ctrl('u') => {
buffer.clear();
insert_mode = false;
update_menu = true;
layout.force_redraw = true;
buffer_updated = true;
},
// Backspace.
Key::Backspace => {
// Delete last char in buffer.
if buffer.pop().is_some() {
if buffer.is_empty() {
insert_mode = false;
update_menu = true;
layout.force_redraw = true;
}
}
buffer_updated = true;
},
// Forward every char if in insert mode.
Key::Char(c) if insert_mode => {
buffer.push(c);
buffer_updated = true;
},
// Reset clock on 'r'. // Reset clock on 'r'.
Key::Char('r') => { Key::Char('r') => {
clock.reset(); clock.reset();
@ -180,33 +222,12 @@ fn main() {
// Jump to the start of the main loop. // Jump to the start of the main loop.
continue; continue;
}, },
// Enter.
Key::Char('\n') => {
if !buffer.is_empty() {
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.set_roster_width(alarm_roster.width());
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.
if buffer.pop().is_some() { buffer_updated = true };
},
Key::Char(c) => { Key::Char(c) => {
if c.is_ascii_digit() { if c.is_ascii_digit() {
buffer.push(c); buffer.push(c);
insert_mode = true;
update_menu = true;
layout.force_redraw = true;
buffer_updated = true; buffer_updated = true;
} else if !buffer.is_empty() && c == ':' { } else if !buffer.is_empty() && c == ':' {
buffer.push(':'); buffer.push(':');
@ -220,7 +241,7 @@ fn main() {
// Update input buffer display. // Update input buffer display.
if buffer_updated { if buffer_updated {
draw_buffer(&mut stdout, &layout, &buffer); draw_buffer(&mut stdout, &mut layout, &buffer);
buffer_updated = false; buffer_updated = false;
stdout.flush().unwrap(); stdout.flush().unwrap();
} }
@ -274,27 +295,34 @@ fn main() {
// Clear the window and redraw menu bar, alarm roster and buffer if // Clear the window and redraw menu bar, alarm roster and buffer if
// requested. // requested.
if layout.force_redraw { if layout.force_redraw {
write!(stdout, write!(stdout, "{}", clear::All).unwrap();
"{}{}{}{}{}",
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. // Redraw list of alarms.
alarm_roster.draw(&mut stdout, &mut layout); alarm_roster.draw(&mut stdout, &mut layout);
// Redraw buffer. // Redraw buffer.
draw_buffer(&mut stdout, &layout, &buffer); draw_buffer(&mut stdout, &mut layout, &buffer);
// Schedule menu redraw.
update_menu = true;
}
if update_menu {
update_menu = false;
write!(stdout,
"{}{}{}{}",
cursor::Goto(1, 1),
style::Faint,
// Switch menu bars. Use a compressed version or none at
// all if necessary.
match insert_mode {
true if layout.can_hold(MENUBAR_INS) => MENUBAR_INS,
false if layout.can_hold(MENUBAR) => MENUBAR,
false if layout.can_hold(MENUBAR_SHORT) => MENUBAR_SHORT,
_ => "",
},
style::Reset,)
.unwrap();
} }
clock.draw(&mut stdout, &layout); clock.draw(&mut stdout, &layout);
@ -304,6 +332,15 @@ fn main() {
countdown.draw(&mut stdout); countdown.draw(&mut stdout);
} }
// Move cursor to buffer position.
if insert_mode {
write!(
stdout,
"{}",
cursor::Goto(layout.cursor.col, layout.cursor.line))
.unwrap();
}
// Check any spawned child process. // Check any spawned child process.
if let Some(ref mut child) = spawned { if let Some(ref mut child) = spawned {
match child.try_wait() { match child.try_wait() {
@ -493,22 +530,25 @@ fn restore_after_suspend<W: Write>(stdout: &mut RawTerminal<W>) {
// Draw input buffer. // Draw input buffer.
fn draw_buffer<W: Write>( fn draw_buffer<W: Write>(
stdout: &mut RawTerminal<W>, stdout: &mut RawTerminal<W>,
layout: &Layout, layout: &mut Layout,
buffer: &String, buffer: &String,
) { ) {
if !buffer.is_empty() { if !buffer.is_empty() {
write!(stdout, write!(stdout,
"{}{}Add alarm: {}", "{}{}Add alarm: {}{}",
cursor::Goto(layout.buffer.col, layout.buffer.line), cursor::Goto(layout.buffer.col, layout.buffer.line),
clear::CurrentLine, clear::CurrentLine,
cursor::Show,
buffer) buffer)
.unwrap(); .unwrap();
layout.cursor.col = layout.buffer.col + 11 + str_length(buffer);
} else { } else {
// Clear buffer display. // Clear buffer display.
write!(stdout, write!(stdout,
"{}{}", "{}{}{}",
cursor::Goto(layout.buffer.col, layout.buffer.line), cursor::Goto(layout.buffer.col, layout.buffer.line),
clear::CurrentLine) clear::CurrentLine,
cursor::Hide)
.unwrap(); .unwrap();
} }
} }
@ -516,11 +556,12 @@ fn draw_buffer<W: Write>(
// Draw error message. // Draw error message.
fn error_msg<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, msg: &str) { fn error_msg<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, msg: &str) {
write!(stdout, write!(stdout,
"{}{}{}{}", "{}{}{}{}{}",
cursor::Goto(layout.error.col, layout.error.line), cursor::Goto(layout.error.col, layout.error.line),
color::Fg(color::LightRed), color::Fg(color::LightRed),
msg, msg,
color::Fg(color::Reset)) color::Fg(color::Reset),
cursor::Hide)
.unwrap(); .unwrap();
} }