FFFF Alpha TUI: fixes from the owner's trial of flahax mix by rafatahmed · Pull Request #136 · rafatahmed/Flahax · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/flahax/grower_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ def number(value, digits=4):
return 'Not available'
if not isinstance(value, (int, float) 8000 ) or not math.isfinite(value):
return text(value)
if abs(value) < 1e-9:
value = 0.0 # floating-point noise such as 7e-15 mg/L
if value != 0 and abs(value) < 0.0001:
return f'{value:.3e}'
return f'{value:,.{digits}f}'.rstrip('0').rstrip('.') if digits else f'{value:,.0f}'
Expand Down
24 changes: 22 additions & 2 deletions src/flahax/grower_sheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
Every number comes from ``summarize_batch``, i.e. from the recipe that is weighed.
"""

import re

from .grower_report import (
SHEET_STYLE,
STATUS_WORDS,
Expand All @@ -25,6 +27,24 @@
)

LABEL = {'N_NO3': 'NO3-N', 'N_NH4': 'NH4-N', 'N_UREA': 'Urea-N'}
REASON_WORDS = {
'(no_source)': '(no product supplies it)',
'(conflicting_targets)': '(conflicts with other targets)',
}


def readable(warning):
"""A warning in grower words: NO3-N for N_NO3, plain reasons, "pH adjustment:" for a stage."""
stage = re.match(r'([a-z]+(?:_[a-z]+)*): ', warning)
if stage:
name = stage.group(1).replace('_', ' ')
name = 'pH' + name[2:] if name.startswith('ph ') else name.capitalize()
warning = f'{name}: {warning[stage.end():]}'
for code, words in REASON_WORDS.items():
warning = warning.replace(code, words)
return re.sub(r'\bN_(NO3|NH4|UREA)\b', lambda m: LABEL[m.group(0)], warning)


CARD_STYLE = (
SHEET_STYLE
+ """
Expand Down Expand Up @@ -211,7 +231,7 @@ def cell(v, digits=2):
)
else:
adds = ''
reasons = ''.join(f'<li>{text(w)}</li>' for w in summary['warnings'])
reasons = ''.join(f'<li>{text(readable(w))}</li>' for w in summary['warnings'])
return (
table
+ (
Expand All @@ -237,7 +257,7 @@ def render_mix_sheet(summary, depth=1):
blocked = s['status'] == 'blocked'
css = {'ready': 'ok', 'requires_review': 'warn', 'blocked': 'stop'}[s['status']]
supplied = volumes['irrigation_litres'] if volumes else s['batch_litres']
notes = ''.join(f'<li>{text(w)}</li>' for w in key_reasons(s))
notes = ''.join(f'<li>{text(readable(w))}</li>' for w in key_reasons(s))
head = (
f'<h1>{text(s["title"])}</h1><div class="meta">FlahaX {text(s["flahax_version"])} '
f'&middot; '
Expand Down
22 changes: 15 additions & 7 deletions src/flahax/mix_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@
from .engine import load_library
from .grower_report import number, render_report
from .grower_sheet import (
LABEL,
STATUS_WORDS,
grams,
key_reasons,
limited_by,
litres,
millilitres,
readable,
render_mix_sheet,
) # noqa: F401 (STATUS_WORDS re-exported)
from .materials import declared_ions_check, load_identities, make_grade, resolve_material
Expand Down Expand Up @@ -175,6 +177,9 @@ def step(self, index, heading=None):
def answered(self, label, value):
"""A finished step's short answer; the line mode shows nothing."""

def progress(self, text):
"""Progress within a step, such as the lab values typed so far; the line mode is silent."""

def new_name(self, store, kind, prompt, default=None):
"""A name for a new saved item; an existing name is used only when the user chooses to
replace it.
Expand Down Expand Up @@ -303,7 +308,7 @@ def ask_formula(p, store, pending, library):
ref = p.search('Crop formula', list(packaged))
targets, title = dict(packaged[ref]), ref
if pick == 1:
changed = {k: p.number(f'{k}, mg/L', v) for k, v in targets.items()}
changed = {k: p.number(f'{LABEL.get(k, k)}, mg/L', v) for k, v in targets.items()}
if changed != targets:
targets, ref = (
changed,
Expand All @@ -319,7 +324,9 @@ def ask_formula(p, store, pending, library):
title = p.typed
p.say('Targets in mg/L; nitrogen as NO3-N and NH4-N. Enter leaves a nutrient untargeted.')
targets = {
k: v for k in FORMULA_KEYS if (v := p.number(f'{k}, mg/L', blank=True)) is not None
k: v
for k in FORMULA_KEYS
if (v := p.number(f'{LABEL.get(k, k)}, mg/L', blank=True)) is not None
}
pending.append(('formulas', ref, targets))
balance = target_ion_balance(targets)
Expand All @@ -330,7 +337,7 @@ def ask_formula(p, store, pending, library):
)
if balance['exceedsWarning']:
p.say(
f'Warning (targetIonBalance): the formula is {abs(balance["imbalancePercent"]):.1f} '
f'Warning: the formula is {abs(balance["imbalancePercent"]):.1f} '
f'% out of balance, '
f'beyond {ION_BALANCE_WARNING_PCT:g} %; no set of salts meets every target, and '
f'unlisted counter-ions '
Expand Down Expand Up @@ -497,7 +504,7 @@ def ask_acids(p, store, pending):
)
fraction = p.number('Assay on the certificate, % w/w', positive=True, high=100) / 100
density = p.number('Density on the certificate, kg/L', positive=True)
while ':' in (name := p.text('Name', f'{formula} {fraction:.0%}')):
while ':' in (name := p.text('Name of this acid', f'{formula} {fraction:.0%}')):
p.say(' A name cannot contain ":".')
ref = (
p.new_name(store, 'acids', 'Save as', name) if slug(name) in saved else slug(name)
Expand All @@ -510,6 +517,7 @@ def ask_acids(p, store, pending):
)
)
chosen.append(ref)
p.say(f' Added {name} for the pH plan.')
else:
return chosen

Expand Down Expand Up @@ -553,8 +561,8 @@ def interview(p, store):
tanks = spec['tank_litres']
p.answered(
'Tanks',
f'A {tanks["A"]:g} L, B {tanks["B"]:g} L, acid {tanks["acid"]:g} L; '
f'1:{spec["ratio"]:g}, {spec["temperature_c"]:g} °C',
f'A {tanks["A"]:g} L, B {tanks["B"]:g} L\n'
f'acid {tanks["acid"]:g} L, 1:{spec["ratio"]:g}, {spec["temperature_c"]:g} °C',
)
water, share, limits = ask_water(p, store, pending)
p.answered('Water', 'RO' if water == 'ro' else f'{water} ({share:g} %)')
Expand Down Expand Up @@ -623,7 +631,7 @@ def summarize(summary, say):
volumes, ratio = summary['volumes'], summary['tanks']['injection_ratio']
blocked = summary['status'] == 'blocked'
say(f'FlahaX {__version__}: {summary["title"]} - {STATUS_WORDS[summary["status"]]}')
for reason in key_reasons(summary):
for reason in map(readable, key_reasons(summary)):
if len(reason) > 160:
reason = reason[:150].rstrip() + ' ... (full text on the batch sheet)'
say(f' ! {reason}')
Expand Down
45 changes: 25 additions & 20 deletions src/flahax/mix_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@

from . import __version__
from .grower_report import _reagent_line, grams, litres, millilitres, number
from .grower_sheet import LABEL, STATUS_WORDS, agronomist_rows, key_reasons, limited_by
from .grower_sheet import LABEL, STATUS_WORDS, agronomist_rows, key_reasons, limited_by, readable
from .mix_tui import MIN_HEIGHT, MIN_WIDTH, SMALL, cut, paint, wrap

VIEWS = ('Tanks', 'Nutrients', 'Notes')
STATUS_STYLE = {'ready': 'ok', 'requires_review': 'warn', 'blocked': 'error'}
KEYS = 'Tab Next view {up}{down} Scroll Q Quit'
COLUMNS = (('Nutrient', 9), ('Target', 9), ('From water', 11), ('Required', 9))
COLUMNS += (('Achieved', 9), ('Final', 9), ('Diff', 18))
COLUMNS = (('Nutrient', 7), ('Target', 8), ('From water', 10), ('Required', 8))
COLUMNS += (('Achieved', 8), ('Final', 8), ('Diff', 18))


def first_view(summary):
Expand Down Expand Up @@ -63,7 +63,7 @@ def _tank_rows(summary, width, g):
else f'prepare {litres(volumes["prepared_litres"][name])} L in your '
f'{litres(volumes["capacity_litres"][name])} L tank{limited_by(volumes)}'
)
body = [[(head, 'error' if blocked else None)]]
body = [[(line, 'error' if blocked else None)] for line in wrap(head, inner, lines=4)]
for product in tank['products']:
weight = grams(product['gramsPerBatch'])
label = cut(
Expand Down Expand Up @@ -105,16 +105,12 @@ def _tank_rows(summary, width, g):
ratio = summary['tanks']['injection_ratio']
mode = 'prepared full' if volumes.get('mode') == 'fill' else 'one irrigation'
temperature = summary['tanks'].get('temperature_c& 10000 #39;)
rows.append(
[
(
f'1:{ratio:g} injection {g["dot"]} {mode}'
+ (f' {g["dot"]} {temperature:g} {g["deg"]}C' if temperature is not None else '')
+ f' {g["dot"]} {dissolves}',
'dim',
)
]
footer = (
f'1:{ratio:g} injection {g["dot"]} {mode}'
+ (f' {g["dot"]} {temperature:g} {g["deg"]}C' if temperature is not None else '')
+ f' {g["dot"]} {dissolves}'
)
rows.extend([[(line, 'dim')] for line in wrap(footer, width, lines=3)])
return rows


Expand All @@ -125,7 +121,7 @@ def cell(value):
rows = [
[
(
''.join(
' '.join(
name.rjust(w) if i else name.ljust(w) for i, (name, w) in enumerate(COLUMNS)
),
'accent',
Expand All @@ -143,7 +139,7 @@ def cell(value):
cell(r['final']),
diff,
)
line = ''.join(
line = ' '.join(
c.rjust(w) if i else c.ljust(w) for i, (c, (_, w)) in enumerate(zip(cells, COLUMNS))
)
style = 'dim' if r['target'] is None else 'warn' if abs(r['diff_pct']) > 1 else None
Expand All @@ -165,13 +161,16 @@ def _note_rows(summary, width):
rows = []
for warning in ordered:
style = 'error' if warning in blocking else None
for i, line in enumerate(wrap(warning, width - 4, lines=10**6)):
for i, line in enumerate(wrap(readable(warning), width - 4, lines=10**6)):
rows.append([(('- ' if i == 0 else ' ') + line, style)])
return rows or [[('No notes.', 'dim')]]


def results_lines(summary, view, width, height, scroll, g, output):
"""The lines to draw for one results view, scrolled by ``scroll`` rows."""
def results_lines(summary, view, width, height, scroll, g, output, message=None):
"""The lines to draw for one results view, scrolled by ``scroll`` rows.

``message`` (a question such as the leave confirmation) replaces the "Sheets:" line.
"""
if width < MIN_WIDTH or height < MIN_HEIGHT:
return [SMALL]
usable = width - 1
Expand Down Expand Up @@ -199,9 +198,15 @@ def results_lines(summary, view, width, height, scroll, g, output):
body = _note_rows(summary, usable - 2)
room = height - 1 - len(lines) - 2
scroll = max(0, min(scroll, len(body) - room))
for row in body[scroll : scroll + room]:
shown = body[scroll : scroll + room]
if scroll > 0:
shown[0] = [(f'{g["up"]} more above', 'warn')]
if scroll + room < len(body):
shown[-1] = [(f'{g["down"]} more below ({g["up"]}{g["down"]} to scroll)', 'warn')]
for row in shown:
lines.append(paint([(' ', None)] + row, usable, g))
lines += [''] * (height - 3 - len(lines))
lines.append(paint([(f' Sheets: {output}', 'dim')], usable, g))
footer = [(f' {message}', 'warn')] if message else [(f' Sheets: {output}', 'dim')]
lines.append(paint(footer, usable, g))
lines.append(paint([(' ' + KEYS.format(**g), 'dim')], usable, g))
return lines
38 changes: 30 additions & 8 deletions src/flahax/mix_tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@
'ok': '\x1b[92m',
'warn': '\x1b[93m',
'error': '\x1b[91m',
'dim': '\x1b[2m',
'dim': '', # the terminal's normal colour: faint text vanished on some themes
'selected': '\x1b[7m',
None: '',
}
HELP = {
'choice': '{up}{down} Move Enter Choose Esc Quit',
'filter': '{up}{down} Move Type to filter Enter Choose Esc Quit',
'ticks': '{up}{down} Move Space Tick Type to filter Enter Next Esc Quit',
'ticks-filtered': '{up}{down} Move Tab Tick Type to filter Enter Next Esc Quit',
'field': 'Type Backspace Edit Enter Next Esc Quit',
}

Expand Down Expand Up @@ -76,6 +77,7 @@ class Wizard:
step: int = 1
answers: list = field(default_factory=list)
messages: list = field(default_factory=list)
progress: str | None = None # e.g. the lab values typed so far in this step


def visible(question):
Expand Down Expand Up @@ -112,7 +114,7 @@ def handle(question, key, validate=None):
question.selected = 0
elif key == 'END':
question.selected = max(0, len(shown) - 1)
elif key == ' ' and question.kind == 'ticks':
elif question.kind == 'ticks' and (key == 'TAB' or (key == ' ' and not question.query)):
if shown:
index = shown[question.selected]
if index in question.ticked:
Expand All @@ -139,6 +141,13 @@ def handle(question, key, validate=None):
return question


def help_for(question):
"""The key-help template for a question: in a filtered tick list Tab ticks, not Space."""
if question.kind == 'ticks' and question.query:
return HELP['ticks-filtered']
return HELP[question.kind]


def cut(text, width, g):
"""``text`` shortened to ``width`` characters, ending with the ellipsis when cut."""
if len(text) <= width:
Expand Down Expand Up @@ -178,7 +187,7 @@ def steps_pane(wizard, g):
rows = [[('Your batch', 'accent')]]
for label, value in wizard.answers:
rows.append([(f'{g["tick"]} {label}', 'ok')])
rows.append([(f' {value}', None)])
rows.extend([[(f' {line}', None)] for line in str(value).split('\n')])
rows.append([(f'{g["here"]} {STEPS[wizard.step - 1]}', 'accent')])
rows.extend([[(f' {name}', 'dim')] for name in STEPS[wizard.step :]])
return rows
Expand All @@ -200,6 +209,8 @@ def step_bar(wizard, g):
def question_rows(wizard, question, width, height, g):
"""The question area: messages, prompt, filter or field, options and the error line."""
rows = []
if wizard.progress:
rows.extend([[(line, 'ok')] for line in wrap(wizard.progress, width)])
for message in wizard.messages:
rows.extend([[(line, 'warn')] for line in wrap(message.strip(), width)])
if question is None:
Expand Down Expand Up @@ -264,7 +275,7 @@ def frame(wizard, question, width, height, g):
lines += [''] * (height - 2 - len(lines))
lines.append(
paint(
[(' ' + HELP[question.kind].format(**g) if question else 'Esc Quit', 'dim')],
[(' ' + help_for(question).format(**g) if question else 'Esc Quit', 'dim')],
usable,
g,
)
Expand Down Expand Up @@ -391,7 +402,11 @@ def pick_many(self, prompt, names):

def step(self, index, heading=None):
"""Move the step bar to step ``index``."""
self.wizard.step = index
self.wizard.step, self.wizard.progress = index, None

def progress(self, text):
"""Show ``text`` above the questions of this step until it changes (None hides it)."""
self.wizard.progress = text

def answered(self, label, value):
"""Add a finished step's short answer to "Your batch"."""
Expand Down Expand Up @@ -428,10 +443,11 @@ def run():


def show_results(term, g, summary, output, ahead=()):
"""The results screen until Q or Esc; ``ahead`` are keys typed while it was calculated."""
"""The results screen until Q or Esc, then Y; ``ahead``: keys typed during the calculation."""
from .mix_results import first_view, handle_results, results_lines # imports this module

view, scroll, shown, key, ahead = first_view(summary), 0, None, None, list(ahead)
leave = f'Leave the results? The sheets are saved in {output}. Y/N'
while True:
width, height = term.size()
lines = results_lines(summary, view, width, height, scroll, g, str(output))
Expand All @@ -442,8 +458,14 @@ def show_results(term, g, summary, output, ahead=()):
if key is None:
continue
view, scroll, done = handle_results(view, scroll, key)
if done:
return
while done: # a stray Q or Esc must not close the screen: ask first
width, height = term.size()
term.draw(results_lines(summary, view, width, height, scroll, g, str(output), leave))
answer = ahead.pop(0) if ahead else term.read_key()
if answer in ('y', 'Y'):
return
done = answer is None
shown = None


def run_tui(store, session_name, output_arg, term_factory=Terminal, encoding=None):
Expand Down
5 changes: 5 additions & 0 deletions src/flahax/mix_water.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ def keep(ion, value, limit):

for ion in WATER_IONS:
label = label_of(ion)
entered = [f'{i} {v:g}' for i, v in values.items()] + [
f'{i} <{v:g}' for i, v in below.items()
]
p.progress('Entered: ' + ', '.join(entered) if entered else None)
value, limit = ask_ion(p, label)
if ion == 'Alk' and value is None and limit is None:
p.say(
Expand All @@ -146,6 +150,7 @@ def keep(ion, value, limit):
else:
value = 0.0
keep(ion, value, limit)
p.progress(None)
while True:
mg = to_elements(values, units, alkalinity, micro)
cations, anions, apart, parts = water_balance(mg)
Expand Down
Loading
Loading
0