Extensive refactoring.
This commit is contained in:
parent
d264d8540d
commit
1e749c7cbd
7 changed files with 498 additions and 464 deletions
|
@ -2,9 +2,9 @@ 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, Layout, Position};
|
use crate::{Clock, Config, Layout, Position};
|
||||||
use crate::common::{COLOR, LABEL_SIZE_LIMIT};
|
use crate::utils::*;
|
||||||
use crate::common::{Config, unicode_length, unicode_truncate};
|
use crate::consts::{COLOR, LABEL_SIZE_LIMIT};
|
||||||
|
|
||||||
|
|
||||||
pub struct Countdown {
|
pub struct Countdown {
|
||||||
|
@ -86,7 +86,7 @@ impl AlarmRoster {
|
||||||
let time_str: &str;
|
let time_str: &str;
|
||||||
|
|
||||||
if let Some(i) = input.find('/') {
|
if let Some(i) = input.find('/') {
|
||||||
label = input[(i + 1)..].trim().to_string();
|
label = input[(i + 1)..].to_string();
|
||||||
// Truncate label.
|
// Truncate label.
|
||||||
unicode_truncate(&mut label, LABEL_SIZE_LIMIT);
|
unicode_truncate(&mut label, LABEL_SIZE_LIMIT);
|
||||||
time_str = &input[..i].trim();
|
time_str = &input[..i].trim();
|
||||||
|
|
|
@ -2,10 +2,9 @@ use std::time;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use termion::{color, cursor};
|
use termion::{color, cursor};
|
||||||
use termion::raw::RawTerminal;
|
use termion::raw::RawTerminal;
|
||||||
use crate::common::*;
|
use crate::consts::*;
|
||||||
use crate::Layout;
|
use crate::Layout;
|
||||||
use crate::Position;
|
use crate::Position;
|
||||||
use crate::common::COLOR;
|
|
||||||
|
|
||||||
|
|
||||||
pub struct Clock {
|
pub struct Clock {
|
||||||
|
|
|
@ -1,33 +1,48 @@
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
pub const NAME: &str = env!("CARGO_PKG_NAME");
|
||||||
use termion::color;
|
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]]
|
||||||
|
|
||||||
|
PARAMETERS:
|
||||||
|
[ALARM TIME[/LABEL]] Any number of alarm times (HH:MM:SS) with optional
|
||||||
|
label.
|
||||||
|
|
||||||
pub struct Config {
|
OPTIONS:
|
||||||
pub plain: bool,
|
-h, --help Show this help.
|
||||||
pub quit: bool,
|
-v, --version Show version information.
|
||||||
pub command: Option<Vec<String>>,
|
-e, --exec [COMMAND] Execute COMMAND on alarm. Occurrences of {t} will
|
||||||
}
|
be replaced by the alarm time in (HH:)MM:SS format.
|
||||||
|
Occurrences of {l} by alarm label.
|
||||||
|
-p, --plain Use simpler block chars.
|
||||||
|
-q, --quit Quit program after last alarm.
|
||||||
|
|
||||||
pub fn unicode_length(input: &str) -> u16 {
|
SIGNALS: <SIGUSR1> Reset clock.
|
||||||
let length = UnicodeSegmentation::graphemes(input, true).count();
|
<SIGUSR2> Pause or un-pause clock.");
|
||||||
length as u16
|
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 fn unicode_truncate(input: &mut String, limit: usize) {
|
// Needed for signal_hook.
|
||||||
match UnicodeSegmentation::grapheme_indices(input.as_str(), true).nth(limit) {
|
pub const SIGTSTP: usize = signal_hook::consts::SIGTSTP as usize;
|
||||||
Some((i, _)) => input.truncate(i),
|
pub const SIGWINCH: usize = signal_hook::consts::SIGWINCH as usize;
|
||||||
None => (),
|
pub const SIGCONT: usize = signal_hook::consts::SIGCONT as usize;
|
||||||
}
|
pub const SIGTERM: usize = signal_hook::consts::SIGTERM as usize;
|
||||||
}
|
pub const SIGINT: usize = signal_hook::consts::SIGINT as usize;
|
||||||
|
pub const SIGUSR1: usize = signal_hook::consts::SIGUSR1 as usize;
|
||||||
|
pub const SIGUSR2: usize = signal_hook::consts::SIGUSR2 as usize;
|
||||||
|
|
||||||
pub const COLOR: [&dyn color::Color; 6] = [
|
pub const COLOR: [&dyn termion::color::Color; 6] = [
|
||||||
&color::Cyan,
|
&termion::color::Cyan,
|
||||||
&color::Magenta,
|
&termion::color::Magenta,
|
||||||
&color::Green,
|
&termion::color::Green,
|
||||||
&color::Yellow,
|
&termion::color::Yellow,
|
||||||
&color::Blue,
|
&termion::color::Blue,
|
||||||
&color::Red,
|
&termion::color::Red,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Maximum length of labels.
|
// Maximum length of labels.
|
||||||
pub const LABEL_SIZE_LIMIT: usize = 48;
|
pub const LABEL_SIZE_LIMIT: usize = 48;
|
||||||
pub const DIGIT_HEIGHT: u16 = 5;
|
pub const DIGIT_HEIGHT: u16 = 5;
|
410
src/kitchentimer.rs
Normal file
410
src/kitchentimer.rs
Normal file
|
@ -0,0 +1,410 @@
|
||||||
|
extern crate termion;
|
||||||
|
|
||||||
|
use std::{time, thread};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
|
use signal_hook::low_level;
|
||||||
|
use termion::{clear, color, cursor, style};
|
||||||
|
use termion::raw::{IntoRawMode, RawTerminal};
|
||||||
|
use termion::event::Key;
|
||||||
|
use termion::input::TermRead;
|
||||||
|
use crate::clock::Clock;
|
||||||
|
use crate::alarm::{Countdown, AlarmRoster, exec_command};
|
||||||
|
use crate::layout::Layout;
|
||||||
|
use crate::consts::*;
|
||||||
|
use crate::utils::*;
|
||||||
|
use crate::Config;
|
||||||
|
|
||||||
|
pub fn kitchentimer(
|
||||||
|
config: Config,
|
||||||
|
mut alarm_roster: AlarmRoster,
|
||||||
|
signal: Arc<AtomicUsize>,
|
||||||
|
sigwinch: Arc<AtomicBool>,
|
||||||
|
spawned: &mut Option<std::process::Child>,
|
||||||
|
) {
|
||||||
|
let mut layout = Layout::new(&config);
|
||||||
|
layout.force_recalc = sigwinch;
|
||||||
|
// Initialise roster_width.
|
||||||
|
layout.set_roster_width(alarm_roster.width());
|
||||||
|
let mut clock = Clock::new();
|
||||||
|
let mut countdown = Countdown::new();
|
||||||
|
|
||||||
|
let mut buffer = String::new();
|
||||||
|
let mut input_keys = termion::async_stdin().keys();
|
||||||
|
|
||||||
|
// State variables.
|
||||||
|
let mut update_buffer = false;
|
||||||
|
let mut update_menu = false;
|
||||||
|
let mut insert_mode = false;
|
||||||
|
|
||||||
|
let mut stdout = std::io::stdout().into_raw_mode()
|
||||||
|
.unwrap_or_else(|error| {
|
||||||
|
eprintln!("Error opening stdout: {}", error);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear window and hide cursor.
|
||||||
|
write!(stdout,
|
||||||
|
"{}{}",
|
||||||
|
clear::All,
|
||||||
|
cursor::Hide)
|
||||||
|
.unwrap_or_else(|error| {
|
||||||
|
eprintln!("Error writing to stdout: {}", error);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter main loop.
|
||||||
|
loop {
|
||||||
|
// Process received signals.
|
||||||
|
match signal.swap(0, Ordering::Relaxed) {
|
||||||
|
// No signal received.
|
||||||
|
0 => (),
|
||||||
|
// Suspend execution on SIGTSTP.
|
||||||
|
SIGTSTP => {
|
||||||
|
suspend(&mut stdout);
|
||||||
|
// Clear SIGCONT, as we have already taken care to reset the
|
||||||
|
// terminal.
|
||||||
|
signal.compare_and_swap(SIGCONT, 0, Ordering::Relaxed);
|
||||||
|
layout.force_redraw = true;
|
||||||
|
// Jump to the start of the main loop.
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
// Continuing after SIGSTOP.
|
||||||
|
SIGCONT => {
|
||||||
|
// This is reached when the process was suspended by SIGSTOP.
|
||||||
|
restore_after_suspend(&mut stdout);
|
||||||
|
layout.force_redraw = true;
|
||||||
|
},
|
||||||
|
// Exit main loop on SIGTERM and SIGINT.
|
||||||
|
SIGTERM | SIGINT => break,
|
||||||
|
// Reset clock on SIGUSR1.
|
||||||
|
SIGUSR1 => {
|
||||||
|
clock.reset();
|
||||||
|
alarm_roster.reset_all();
|
||||||
|
layout.force_recalc.store(true, Ordering::Relaxed);
|
||||||
|
layout.force_redraw = true;
|
||||||
|
},
|
||||||
|
// (Un-)Pause clock on SIGUSR2.
|
||||||
|
SIGUSR2 => clock.toggle(),
|
||||||
|
// We didn't register anything else.
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process input.
|
||||||
|
if let Some(key) = input_keys.next() {
|
||||||
|
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;
|
||||||
|
update_buffer = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update_buffer = true;
|
||||||
|
},
|
||||||
|
// Forward every char if in insert mode.
|
||||||
|
Key::Char(c) if insert_mode => {
|
||||||
|
buffer.push(c);
|
||||||
|
update_buffer = true;
|
||||||
|
},
|
||||||
|
// Reset clock on 'r'.
|
||||||
|
Key::Char('r') => {
|
||||||
|
clock.reset();
|
||||||
|
alarm_roster.reset_all();
|
||||||
|
layout.force_recalc.store(true, Ordering::Relaxed);
|
||||||
|
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.drop_last() {
|
||||||
|
// If we remove the last alarm we have to reset "countdown"
|
||||||
|
// manually. It is safe to do it anyway.
|
||||||
|
layout.set_roster_width(alarm_roster.width());
|
||||||
|
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);
|
||||||
|
// Clear SIGCONT, as we have already taken care to reset
|
||||||
|
// the terminal.
|
||||||
|
signal.compare_and_swap(SIGCONT, 0, Ordering::Relaxed);
|
||||||
|
layout.force_redraw = true;
|
||||||
|
// Jump to the start of the main loop.
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
Key::Char(c) => {
|
||||||
|
if c.is_ascii_digit() {
|
||||||
|
buffer.push(c);
|
||||||
|
insert_mode = true;
|
||||||
|
update_menu = true;
|
||||||
|
layout.force_redraw = true;
|
||||||
|
update_buffer = true;
|
||||||
|
} else if !buffer.is_empty() && c == ':' {
|
||||||
|
buffer.push(':');
|
||||||
|
update_buffer = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Any other key.
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update input buffer display.
|
||||||
|
if update_buffer {
|
||||||
|
draw_buffer(&mut stdout, &mut layout, &buffer);
|
||||||
|
update_buffer = false;
|
||||||
|
stdout.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed = if clock.paused {
|
||||||
|
clock.elapsed
|
||||||
|
} else {
|
||||||
|
// Should never overflow as we reestablish a new "start"
|
||||||
|
// instant every 24 hours.
|
||||||
|
clock.start.elapsed().as_secs() as u32
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update window 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.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update window size information and calculate the clock position.
|
||||||
|
// Also enforce recalculation of layout if we start displaying
|
||||||
|
// hours.
|
||||||
|
layout.update(clock.elapsed >= 3600, clock.elapsed == 3600);
|
||||||
|
|
||||||
|
// Check for exceeded alarms.
|
||||||
|
if let Some((time, label)) = alarm_roster.check(&mut clock, &layout, &mut countdown) {
|
||||||
|
// Write ASCII bell code.
|
||||||
|
write!(stdout, "{}", 0x07 as char).unwrap();
|
||||||
|
layout.force_redraw = true;
|
||||||
|
|
||||||
|
// Run command if configured.
|
||||||
|
if config.command.is_some() {
|
||||||
|
if spawned.is_none() {
|
||||||
|
*spawned = exec_command(&config, time, &label);
|
||||||
|
} else {
|
||||||
|
// The last command is still running.
|
||||||
|
eprintln!("Not executing command, as its predecessor is still running");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Quit if configured.
|
||||||
|
if config.quit && !alarm_roster.active() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the window and redraw menu bar, alarm roster and buffer if
|
||||||
|
// requested.
|
||||||
|
if layout.force_redraw {
|
||||||
|
write!(stdout, "{}", clear::All).unwrap();
|
||||||
|
|
||||||
|
// Redraw list of alarms.
|
||||||
|
alarm_roster.draw(&mut stdout, &mut layout);
|
||||||
|
|
||||||
|
// Redraw 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);
|
||||||
|
|
||||||
|
// Display countdown.
|
||||||
|
if countdown.value > 0 {
|
||||||
|
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.
|
||||||
|
if let Some(ref mut child) = spawned {
|
||||||
|
match child.try_wait() {
|
||||||
|
// Process exited successfully.
|
||||||
|
Ok(Some(status)) if status.success() => *spawned = None,
|
||||||
|
// Abnormal exit.
|
||||||
|
Ok(Some(status)) => {
|
||||||
|
eprintln!("Spawned process terminated with non-zero exit status. ({})", status);
|
||||||
|
*spawned = None;
|
||||||
|
},
|
||||||
|
// Process is still running.
|
||||||
|
Ok(None) => (),
|
||||||
|
// Other error.
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Error executing command. ({})", error);
|
||||||
|
*spawned = None;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 window and restore cursor.
|
||||||
|
write!(stdout,
|
||||||
|
"{}{}{}",
|
||||||
|
clear::BeforeCursor,
|
||||||
|
cursor::Goto(1, 1),
|
||||||
|
cursor::Show)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw input buffer.
|
||||||
|
fn draw_buffer<W: Write>(
|
||||||
|
stdout: &mut RawTerminal<W>,
|
||||||
|
layout: &mut Layout,
|
||||||
|
buffer: &String,
|
||||||
|
) {
|
||||||
|
if !buffer.is_empty() {
|
||||||
|
write!(stdout,
|
||||||
|
"{}{}Add alarm: {}{}",
|
||||||
|
cursor::Goto(layout.buffer.col, layout.buffer.line),
|
||||||
|
clear::CurrentLine,
|
||||||
|
cursor::Show,
|
||||||
|
buffer)
|
||||||
|
.unwrap();
|
||||||
|
layout.cursor.col = layout.buffer.col + 11 + unicode_length(buffer);
|
||||||
|
} else {
|
||||||
|
// Clear buffer display.
|
||||||
|
write!(stdout,
|
||||||
|
"{}{}{}",
|
||||||
|
cursor::Goto(layout.buffer.col, layout.buffer.line),
|
||||||
|
clear::CurrentLine,
|
||||||
|
cursor::Hide)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw error message at input buffer position.
|
||||||
|
fn error_msg<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, msg: &str) {
|
||||||
|
write!(stdout,
|
||||||
|
"{}{}{}{}{}",
|
||||||
|
cursor::Goto(layout.error.col, layout.error.line),
|
||||||
|
color::Fg(color::LightRed),
|
||||||
|
msg,
|
||||||
|
color::Fg(color::Reset),
|
||||||
|
cursor::Hide)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare to suspend execution. Called on SIGTSTP.
|
||||||
|
fn suspend<W: Write>(mut 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(error) = low_level::emulate_default_handler(SIGTSTP as i32) {
|
||||||
|
eprintln!("Error raising SIGTSTP: {}", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_after_suspend(&mut stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up terminal after SIGTSTP or SIGSTOP.
|
||||||
|
fn restore_after_suspend<W: Write>(stdout: &mut RawTerminal<W>) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use crate::common::*;
|
use crate::Config;
|
||||||
|
use crate::consts::*;
|
||||||
|
|
||||||
// If screen size falls below these values we skip computation of new
|
// If screen size falls below these values we skip computation of new
|
||||||
// positions.
|
// positions.
|
||||||
|
|
460
src/main.rs
460
src/main.rs
|
@ -1,62 +1,30 @@
|
||||||
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 consts;
|
||||||
|
mod kitchentimer;
|
||||||
mod layout;
|
mod layout;
|
||||||
|
mod utils;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use std::{time, thread, env};
|
use std::env;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicUsize};
|
||||||
use signal_hook::{flag, low_level};
|
use signal_hook::flag;
|
||||||
use termion::{clear, color, cursor, style};
|
|
||||||
use termion::raw::{IntoRawMode, RawTerminal};
|
|
||||||
use termion::event::Key;
|
|
||||||
use termion::input::TermRead;
|
|
||||||
use clock::Clock;
|
use clock::Clock;
|
||||||
use alarm::{Countdown, AlarmRoster, exec_command};
|
use alarm::AlarmRoster;
|
||||||
use layout::{Layout, Position};
|
use layout::{Layout, Position};
|
||||||
use common::{Config, unicode_length};
|
use consts::*;
|
||||||
|
use kitchentimer::kitchentimer;
|
||||||
|
|
||||||
|
|
||||||
const NAME: &str = env!("CARGO_PKG_NAME");
|
pub struct Config {
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
plain: bool,
|
||||||
const USAGE: &str = concat!("USAGE: ", env!("CARGO_PKG_NAME"),
|
quit: bool,
|
||||||
" [-h|-v] [-e|--exec COMMAND] [-p] [-q] [ALARM[/LABEL]]
|
command: Option<Vec<String>>,
|
||||||
|
}
|
||||||
PARAMETERS:
|
|
||||||
[ALARM TIME[/LABEL]] Any number of alarm times (HH:MM:SS) with optional
|
|
||||||
label.
|
|
||||||
|
|
||||||
OPTIONS:
|
|
||||||
-h, --help Show this help.
|
|
||||||
-v, --version Show version information.
|
|
||||||
-e, --exec [COMMAND] Execute COMMAND on alarm. Occurrences of {t} will
|
|
||||||
be replaced by the alarm time in (HH:)MM:SS format.
|
|
||||||
Occurrences of {l} by alarm label.
|
|
||||||
-p, --plain Use simpler block chars.
|
|
||||||
-q, --quit Quit program after last alarm.
|
|
||||||
|
|
||||||
SIGNALS: <SIGUSR1> Reset clock.
|
|
||||||
<SIGUSR2> Pause or un-pause clock.");
|
|
||||||
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";
|
|
||||||
const MENUBAR_INS: &str =
|
|
||||||
"Format: HH:MM:SS/LABEL [ENTER] Accept [ESC] Cancel [CTR-C] Quit";
|
|
||||||
// Needed for signal_hook.
|
|
||||||
const SIGTSTP: usize = signal_hook::consts::SIGTSTP as usize;
|
|
||||||
const SIGWINCH: usize = signal_hook::consts::SIGWINCH as usize;
|
|
||||||
const SIGCONT: usize = signal_hook::consts::SIGCONT as usize;
|
|
||||||
const SIGTERM: usize = signal_hook::consts::SIGTERM as usize;
|
|
||||||
const SIGINT: usize = signal_hook::consts::SIGINT as usize;
|
|
||||||
const SIGUSR1: usize = signal_hook::consts::SIGUSR1 as usize;
|
|
||||||
const SIGUSR2: usize = signal_hook::consts::SIGUSR2 as usize;
|
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -66,322 +34,24 @@ fn main() {
|
||||||
command: None,
|
command: None,
|
||||||
};
|
};
|
||||||
let mut alarm_roster = AlarmRoster::new();
|
let mut alarm_roster = AlarmRoster::new();
|
||||||
|
// Parse command line arguments into config and alarm roster.
|
||||||
parse_args(&mut config, &mut alarm_roster);
|
parse_args(&mut config, &mut alarm_roster);
|
||||||
|
|
||||||
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 buffer = String::new();
|
|
||||||
let mut buffer_updated = false;
|
|
||||||
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().
|
|
||||||
let mut spawned: Option<std::process::Child> = None;
|
|
||||||
|
|
||||||
// Initialise roster_width.
|
|
||||||
layout.set_roster_width(alarm_roster.width());
|
|
||||||
|
|
||||||
// Register signal handlers.
|
// Register signal handlers.
|
||||||
let signal = Arc::new(AtomicUsize::new(0));
|
let signal = Arc::new(AtomicUsize::new(0));
|
||||||
register_signal_handlers(&signal, &layout.force_recalc);
|
let sigwinch = Arc::new(AtomicBool::new(true));
|
||||||
|
register_signal_handlers(&signal, &sigwinch);
|
||||||
|
// Spawned child process if any.
|
||||||
|
let mut spawned: Option<std::process::Child> = None;
|
||||||
|
|
||||||
// Clear window and hide cursor.
|
// Runs main loop.
|
||||||
write!(stdout,
|
kitchentimer(
|
||||||
"{}{}",
|
config,
|
||||||
clear::All,
|
alarm_roster,
|
||||||
cursor::Hide)
|
signal,
|
||||||
.unwrap_or_else(|error| {
|
sigwinch,
|
||||||
eprintln!("Error writing to stdout: {}", error);
|
&mut spawned,
|
||||||
std::process::exit(1);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// Enter main loop.
|
|
||||||
loop {
|
|
||||||
// Process received signals.
|
|
||||||
match signal.swap(0, Ordering::Relaxed) {
|
|
||||||
// No signal received.
|
|
||||||
0 => (),
|
|
||||||
// Suspend execution on SIGTSTP.
|
|
||||||
SIGTSTP => {
|
|
||||||
suspend(&mut stdout);
|
|
||||||
// Clear SIGCONT, as we have already taken care to reset the
|
|
||||||
// terminal.
|
|
||||||
signal.compare_and_swap(SIGCONT, 0, Ordering::Relaxed);
|
|
||||||
layout.force_redraw = true;
|
|
||||||
// Jump to the start of the main loop.
|
|
||||||
continue;
|
|
||||||
},
|
|
||||||
// Continuing after SIGSTOP.
|
|
||||||
SIGCONT => {
|
|
||||||
// This is reached when the process was suspended by SIGSTOP.
|
|
||||||
restore_after_suspend(&mut stdout);
|
|
||||||
layout.force_redraw = true;
|
|
||||||
},
|
|
||||||
// Exit main loop on SIGTERM and SIGINT.
|
|
||||||
SIGTERM | SIGINT => break,
|
|
||||||
// Reset clock on SIGUSR1.
|
|
||||||
SIGUSR1 => {
|
|
||||||
clock.reset();
|
|
||||||
alarm_roster.reset_all();
|
|
||||||
layout.force_recalc.store(true, Ordering::Relaxed);
|
|
||||||
layout.force_redraw = true;
|
|
||||||
},
|
|
||||||
// (Un-)Pause clock on SIGUSR2.
|
|
||||||
SIGUSR2 => clock.toggle(),
|
|
||||||
// We didn't register anything else.
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process input.
|
|
||||||
if let Some(key) = input_keys.next() {
|
|
||||||
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'.
|
|
||||||
Key::Char('r') => {
|
|
||||||
clock.reset();
|
|
||||||
alarm_roster.reset_all();
|
|
||||||
layout.force_recalc.store(true, Ordering::Relaxed);
|
|
||||||
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.drop_last() {
|
|
||||||
// If we remove the last alarm we have to reset "countdown"
|
|
||||||
// manually. It is safe to do it anyway.
|
|
||||||
layout.set_roster_width(alarm_roster.width());
|
|
||||||
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);
|
|
||||||
// Clear SIGCONT, as we have already taken care to reset
|
|
||||||
// the terminal.
|
|
||||||
signal.compare_and_swap(SIGCONT, 0, Ordering::Relaxed);
|
|
||||||
layout.force_redraw = true;
|
|
||||||
// Jump to the start of the main loop.
|
|
||||||
continue;
|
|
||||||
},
|
|
||||||
Key::Char(c) => {
|
|
||||||
if c.is_ascii_digit() {
|
|
||||||
buffer.push(c);
|
|
||||||
insert_mode = true;
|
|
||||||
update_menu = true;
|
|
||||||
layout.force_redraw = true;
|
|
||||||
buffer_updated = true;
|
|
||||||
} else if !buffer.is_empty() && c == ':' {
|
|
||||||
buffer.push(':');
|
|
||||||
buffer_updated = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Any other key.
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update input buffer display.
|
|
||||||
if buffer_updated {
|
|
||||||
draw_buffer(&mut stdout, &mut layout, &buffer);
|
|
||||||
buffer_updated = false;
|
|
||||||
stdout.flush().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let elapsed = if clock.paused {
|
|
||||||
clock.elapsed
|
|
||||||
} else {
|
|
||||||
// Should never overflow as we reestablish a new "start"
|
|
||||||
// instant every 24 hours.
|
|
||||||
clock.start.elapsed().as_secs() as u32
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update window 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.store(true, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update window size information and calculate the clock position.
|
|
||||||
// Also enforce recalculation of layout if we start displaying
|
|
||||||
// hours.
|
|
||||||
layout.update(clock.elapsed >= 3600, clock.elapsed == 3600);
|
|
||||||
|
|
||||||
// Check for exceeded alarms.
|
|
||||||
if let Some((time, label)) = alarm_roster.check(&mut clock, &layout, &mut countdown) {
|
|
||||||
// Write ASCII bell code.
|
|
||||||
write!(stdout, "{}", 0x07 as char).unwrap();
|
|
||||||
layout.force_redraw = true;
|
|
||||||
|
|
||||||
// Run command if configured.
|
|
||||||
if config.command.is_some() {
|
|
||||||
if spawned.is_none() {
|
|
||||||
spawned = exec_command(&config, time, &label);
|
|
||||||
} else {
|
|
||||||
// The last command is still running.
|
|
||||||
eprintln!("Not executing command, as its predecessor is still running");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Quit if configured.
|
|
||||||
if config.quit && !alarm_roster.active() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the window and redraw menu bar, alarm roster and buffer if
|
|
||||||
// requested.
|
|
||||||
if layout.force_redraw {
|
|
||||||
write!(stdout, "{}", clear::All).unwrap();
|
|
||||||
|
|
||||||
// Redraw list of alarms.
|
|
||||||
alarm_roster.draw(&mut stdout, &mut layout);
|
|
||||||
|
|
||||||
// Redraw 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);
|
|
||||||
|
|
||||||
// Display countdown.
|
|
||||||
if countdown.value > 0 {
|
|
||||||
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.
|
|
||||||
if let Some(ref mut child) = spawned {
|
|
||||||
match child.try_wait() {
|
|
||||||
// Process exited successfully.
|
|
||||||
Ok(Some(status)) if status.success() => spawned = None,
|
|
||||||
// Abnormal exit.
|
|
||||||
Ok(Some(status)) => {
|
|
||||||
eprintln!("Spawned process terminated with non-zero exit status. ({})", status);
|
|
||||||
spawned = None;
|
|
||||||
},
|
|
||||||
// Process is still running.
|
|
||||||
Ok(None) => (),
|
|
||||||
// Other error.
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("Error executing command. ({})", error);
|
|
||||||
spawned = None;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 window and restore cursor.
|
|
||||||
write!(stdout,
|
|
||||||
"{}{}{}",
|
|
||||||
clear::BeforeCursor,
|
|
||||||
cursor::Goto(1, 1),
|
|
||||||
cursor::Show)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Reset terminal.
|
|
||||||
drop(stdout);
|
|
||||||
drop(input_keys);
|
|
||||||
|
|
||||||
// Wait for remaining spawned processes to exit.
|
// Wait for remaining spawned processes to exit.
|
||||||
if let Some(ref mut child) = spawned {
|
if let Some(ref mut child) = spawned {
|
||||||
|
@ -490,79 +160,3 @@ fn register_signal_handlers(
|
||||||
flag::register(SIGWINCH as i32, Arc::clone(&recalc_flag)).unwrap();
|
flag::register(SIGWINCH as i32, Arc::clone(&recalc_flag)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare to suspend execution. Called on SIGTSTP.
|
|
||||||
fn suspend<W: Write>(mut 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);
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Err(error) = low_level::emulate_default_handler(SIGTSTP as i32) {
|
|
||||||
eprintln!("Error raising SIGTSTP: {}", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
restore_after_suspend(&mut stdout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up terminal after SIGTSTP or SIGSTOP.
|
|
||||||
fn restore_after_suspend<W: Write>(stdout: &mut RawTerminal<W>) {
|
|
||||||
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: &mut Layout,
|
|
||||||
buffer: &String,
|
|
||||||
) {
|
|
||||||
if !buffer.is_empty() {
|
|
||||||
write!(stdout,
|
|
||||||
"{}{}Add alarm: {}{}",
|
|
||||||
cursor::Goto(layout.buffer.col, layout.buffer.line),
|
|
||||||
clear::CurrentLine,
|
|
||||||
cursor::Show,
|
|
||||||
buffer)
|
|
||||||
.unwrap();
|
|
||||||
layout.cursor.col = layout.buffer.col + 11 + unicode_length(buffer);
|
|
||||||
} else {
|
|
||||||
// Clear buffer display.
|
|
||||||
write!(stdout,
|
|
||||||
"{}{}{}",
|
|
||||||
cursor::Goto(layout.buffer.col, layout.buffer.line),
|
|
||||||
clear::CurrentLine,
|
|
||||||
cursor::Hide)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw error message.
|
|
||||||
fn error_msg<W: Write>(stdout: &mut RawTerminal<W>, layout: &Layout, msg: &str) {
|
|
||||||
write!(stdout,
|
|
||||||
"{}{}{}{}{}",
|
|
||||||
cursor::Goto(layout.error.col, layout.error.line),
|
|
||||||
color::Fg(color::LightRed),
|
|
||||||
msg,
|
|
||||||
color::Fg(color::Reset),
|
|
||||||
cursor::Hide)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
15
src/utils.rs
Normal file
15
src/utils.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
extern crate unicode_segmentation;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
pub fn unicode_length(input: &str) -> u16 {
|
||||||
|
let length = UnicodeSegmentation::graphemes(input, true).count();
|
||||||
|
length as u16
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unicode_truncate(input: &mut String, limit: usize) {
|
||||||
|
match UnicodeSegmentation::grapheme_indices(input.as_str(), true).nth(limit) {
|
||||||
|
Some((i, _)) => input.truncate(i),
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue