IFGIYSTX7BM42O6Q5U7G246MZ5SRIWK65IVHMEG3CN6RSK5FDOCQC
use std::{cell::RefCell, fs::File, fs::OpenOptions, io::BufWriter, io::Write};
use termion::{input::MouseTerminal, raw::RawTerminal, screen::AlternateScreen};
pub struct Terminal {
pub inpevs: termion::input::Events<File>,
pub outp: RefCell<AlternateScreen<MouseTerminal<RawTerminal<BufWriter<File>>>>>,
}
/// Set the given file to be read in non-blocking mode. That is, attempting a
/// read on the given file may return 0 bytes.
///
/// Copied from private function at https://docs.rs/nonblock/0.1.0/nonblock/.
///
/// The MIT License (MIT)
///
/// Copyright (c) 2016 Anthony Nowell
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the "Software"), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in all
/// copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
/// SOFTWARE.
#[cfg(unix)]
fn set_blocking(file: &File, blocking: bool) -> std::io::Result<()> {
use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK};
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
let flags = unsafe { fcntl(fd, F_GETFL, 0) };
if flags < 0 {
return Err(std::io::Error::last_os_error());
}
let flags = if blocking {
flags & !O_NONBLOCK
} else {
flags | O_NONBLOCK
};
let res = unsafe { fcntl(fd, F_SETFL, flags) };
if res != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
impl Terminal {
pub fn try_new() -> std::io::Result<Self> {
use termion::{input::TermRead, raw::IntoRawMode};
let term_out = OpenOptions::new().write(true).open("/dev/tty")?;
let term_in = OpenOptions::new().read(true).open("/dev/tty")?;
set_blocking(&term_in, false)?;
let inpevs = term_in.events();
let outp = RefCell::new(AlternateScreen::from(MouseTerminal::from(
BufWriter::with_capacity(8_388_608, term_out).into_raw_mode()?,
)));
Ok(Self { inpevs, outp })
}
pub fn rewrite_line(&self, s: &str) -> std::io::Result<()> {
let mut x = self.outp.borrow_mut();
write!(&mut x, "\r{}{}", s, termion::clear::UntilNewline)?;
x.flush()?;
Ok(())
}
pub fn finish(self) -> std::io::Result<()> {
let mut x = self.outp.borrow_mut();
write!(self.outp.borrow_mut(), "\n")?;
x.flush()?;
Ok(())
}
}
fn get_meter_path(base: &Path, name: &str) -> PathBuf {
let mut path = base.join(name);
path.set_extension(METER_LEXT);
path
pub fn log_path(&self) -> PathBuf {
let mut path = self.base.join(&self.name);
path.set_extension("metercsv");
path
}
pub fn read_logdata(&self) -> Result<BTreeSet<LogEntry>, Error> {
csv::ReaderBuilder::new()
.delimiter(0x1f)
.from_path(self.log_path())?
.deserialize()
.map(|i| i.map_err(Into::into))
.collect()
}
pub fn write_logdata(&self, data: &BTreeSet<LogEntry>) -> Result<(), Error> {
use std::io::Write;
let f = tempfile::NamedTempFile::new_in(&self.base).map_err(csv::Error::from)?;
let mut f = csv::WriterBuilder::new().delimiter(0x1f).from_writer(f);
for i in data {
f.serialize(i)?;
}
// get the NamedTempFile back
let mut f = f.into_inner()?;
f.flush().map_err(csv::Error::from)?;
f.persist(self.log_path())?;
Ok(())
}
}
#[derive(Clone, Copy)]
pub enum ValidateErrorKind {
InvalidValueDir,
pub fn parse_logdata(bpath: &Path, name: &str) -> Result<Vec<LogEntry>, Error> {
csv::ReaderBuilder::new()
.delimiter(0x1f)
.from_path(get_meter_path(bpath, name))?
.deserialize()
.map(|i| i.map_err(Into::into))
.collect()
pub struct ValidateError<'e> {
a: &'e LogEntry,
b: &'e LogEntry,
kind: ValidateErrorKind,
pub fn write_logdata(bpath: &Path, name: &str, data: &[LogEntry]) -> Result<(), Error> {
use std::io::Write;
let f = tempfile::NamedTempFile::new_in(bpath).map_err(csv::Error::from)?;
let path = get_meter_path(bpath, name);
let mut f = csv::WriterBuilder::new().delimiter(0x1f).from_writer(f);
for i in data {
f.serialize(i)?;
impl fmt::Display for ValidateError<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ValidateErrorKind as K;
writeln!(
f,
"log error: {}\n a = {:?}\n b = {:?}",
match self.kind {
K::InvalidValueDir => "invalid value direction",
},
self.a,
self.b
)
// get the NamedTempFile back
let mut f = f.into_inner()?;
f.flush().map_err(csv::Error::from)?;
f.persist(path)?;
Ok(())
}
pub fn validate_logdata<'a>(
logdata: &'a BTreeSet<LogEntry>,
limit_date: Option<&NaiveDate>,
) -> Vec<ValidateError<'a>> {
let ldv: Vec<&_> = logdata.iter().collect();
ldv.windows(2)
.map(|i| match i {
[a, b] => (a, b),
_ => unreachable!(),
})
.filter(|(a, b)| {
if let Some(x) = limit_date {
if !(&a.date == x || &b.date == x) {
return false;
}
}
true
})
.filter_map(|(a, b)| {
use LogCommand as C;
use ValidateErrorKind as K;
Some(ValidateError {
a,
b,
kind: match (a.cmd, b.cmd) {
(C::Increase, C::Increase) if a.value >= b.value => K::InvalidValueDir,
(C::Decrease, C::Decrease) if a.value <= b.value => K::InvalidValueDir,
_ => return None,
},
})
})
.collect()
use std::io::Write;
use zs_utilinv_core::countermeter::{
validate_logdata, FixpNum, Handle as CmHandle, LogCommand, LogEntry, NaiveDate,
};
struct ViewIo {
iatty: bool,
stdin: std::io::Stdin,
stdout: std::io::Stdout,
buf: String,
}
impl ViewIo {
fn prompt(&mut self, q: &str) -> std::io::Result<&str> {
if self.iatty {
write!(&mut self.stdout, "{} $ ", q)?;
self.stdout.flush()?;
}
self.buf.clear();
self.stdin.read_line(&mut self.buf)?;
if !self.iatty {
writeln!(&mut self.stdout)?;
}
Ok(self.buf.trim())
}
fn prompt_yn(&mut self, q: &str) -> std::io::Result<bool> {
let q2 = format!("{} ? [yn]", q);
Ok(loop {
break match self.prompt(&q2)?.to_lowercase().as_str() {
"y" | "yes" | "j" | "ja" => true,
"n" | "no" | "nein" => false,
_ => continue,
};
})
}
}
fn main() -> anyhow::Result<()> {
let mut vio = ViewIo {
iatty: atty::is(atty::Stream::Stdin) && atty::is(atty::Stream::Stdout),
stdin: std::io::stdin(),
stdout: std::io::stdout(),
buf: String::new(),
};
std::fs::create_dir_all("cometers")?;
let date: NaiveDate = loop {
let tmp = vio.prompt("enter the date to edit")?;
if tmp.is_empty() {
return Ok(());
}
if let Ok(x) = NaiveDate::parse_from_str(tmp, "%Y-%m-%d") {
break x;
}
};
loop {
let meter = vio.prompt("meter")?;
if meter.is_empty() {
break;
}
let meter = CmHandle::new(std::path::PathBuf::from("cometers"), meter.to_string());
let mut meterlog = if !meter.log_path().exists() {
eprintln!("ERROR: countermeter «{}» doesn't exist.", meter.name());
if !vio.prompt_yn("would you like to init this cometer log")? {
continue;
}
Default::default()
} else {
meter.read_logdata()?
};
for i in validate_logdata(&meterlog, Some(&date)) {
println!("{}", i);
}
let use_res = on_meter(&mut vio, &date, meter.name(), &mut meterlog)?;
if !use_res {
// do nothing
} else if meterlog.is_empty() {
if vio.prompt_yn("WARN: would you like to clear+remove this cometer log")? {
std::fs::remove_file(meter.log_path())?;
}
} else {
for i in validate_logdata(&meterlog, Some(&date)) {
println!("{}", i);
}
meter.write_logdata(&meterlog)?;
}
}
Ok(())
}
#[derive(Clone)]
enum MeterAct {
Value(Option<FixpNum>),
Command(LogCommand),
Comment(String),
}
fn on_meter(
vio: &mut ViewIo,
date: &NaiveDate,
meter: &str,
meterlog: &mut std::collections::BTreeSet<LogEntry>,
) -> std::io::Result<bool> {
let prpt = format!("cm:{}", meter);
for i in meterlog.iter().filter(|i| &i.date == date) {
println!("{:?}", i);
}
Ok(loop {
let tmp = match vio.prompt(&prpt)? {
"" | "exit" | "quit" => break true,
"qwos" => break false,
"brk" => MeterAct::Command(LogCommand::Broken),
"rst" => MeterAct::Command(LogCommand::Reset),
"dec" => MeterAct::Command(LogCommand::Decrease),
"inc" => MeterAct::Command(LogCommand::Increase),
"-" => MeterAct::Value(None),
"dlt" => {
meterlog.retain(|i| &i.date != date);
continue;
}
"p" => {
for i in meterlog.iter().filter(|i| &i.date == date) {
println!("{:?}", i);
}
continue;
}
"?" | "help" => {
println!(
r#"available log line types:
brk .... broken
rst .... reset
dec .... decrease
inc .... increase
- .... remove the registered value
dlt .... delete the log line
? .... print this help text
exit ... exit this sub-repl, save updated entries
qwos ... exit this sub-repl, don't save updates entries
p .... print the current entry
other allowed inputs:
- a float number (must start with a digit)
- "cmt " and then a comment
"#
);
continue;
}
tmp => {
if tmp
.chars()
.next()
.map(|i| i.is_ascii_digit())
.unwrap_or(false)
{
match tmp.parse::<FixpNum>() {
Ok(x) => MeterAct::Value(Some(x)),
Err(e) => {
eprintln!("ERROR: unable to parse float number: {}", e);
continue;
}
}
} else if let Some(x) = tmp.strip_prefix("cmt ") {
MeterAct::Comment(x.to_string())
} else {
eprintln!("ERROR: unknown meter action: {}", tmp);
continue;
}
}
};
let mut cnt = 0;
*meterlog = std::mem::take(meterlog)
.into_iter()
.map(|mut i| {
if &i.date == date {
match tmp.clone() {
MeterAct::Command(x) => i.cmd = x,
MeterAct::Value(x) => i.value = x,
MeterAct::Comment(x) => i.comment = x,
}
cnt += 1;
}
i
})
.collect();
if cnt == 0 {
let mut i = LogEntry {
date: date.clone(),
cmd: LogCommand::Broken,
comment: String::new(),
value: None,
};
match tmp {
MeterAct::Command(x) => i.cmd = x,
MeterAct::Value(x) => i.value = x,
MeterAct::Comment(x) => i.comment = x,
}
meterlog.insert(i);
}
for i in validate_logdata(&meterlog, Some(&date)) {
println!("{}", i);
}
})
}
[package]
name = "zs-utilinv-cometer"
description = "countermeter input/edit tool"
version = "0.1.0"
edition = "2018"
license = "Apache-2.0"
[dependencies]
anyhow = "1.0"
atty = "0.2"
zs-utilinv-core = { path = "../zs-utilinv-core" }
[[package]]
name = "anyhow"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
/cometers