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 backlet 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 backlet 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 .... brokenrst .... resetdec .... decreaseinc .... increase- .... remove the registered valuedlt .... delete the log line? .... print this help textexit ... exit this sub-repl, save updated entriesqwos ... exit this sub-repl, don't save updates entriesp .... print the current entryother 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