Bilingua ======== :: import streamlit as st import os import pickle import yaml from pathlib import Path import re from datetime import datetime Print banner :: st.set_page_config( page_title="Bilingua", layout="wide" ) @st.cache_data def print_banner(): print(""" _ _ _ _ | |__ (_) (_)_ __ __ _ _ _ __ _ | '_ \\| | | | '_ \\ / _` | | | |/ _` | | |_) | | | | | | | (_| | |_| | (_| | |_.__/|_|_|_|_| |_|\\__, |\\__,_|\\__,_| |___/ """) return 1 print_banner() Init state :: DEFAULTS = { "page": 0, "ptr": 0, } state_file = "bi.state" config_file = "bi.yml" page_len = 5 Serialize selected Streamlit session state variables to a pickle file. :: def save_session_state(file_path: str = state_file) -> None: data = {k: st.session_state.get(k, DEFAULTS[k]) for k in DEFAULTS} with open(file_path, "wb") as f: pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL) Load session state variables from a pickle file if it exists. If not found or on error, assign default values. :: def load_session_state(file_path: str = state_file) -> None: loaded = {} if os.path.exists(file_path): try: with open(file_path, "rb") as f: obj = pickle.load(f) if isinstance(obj, dict): loaded = obj except Exception: loaded = {} for key, default in DEFAULTS.items(): st.session_state[key] = loaded.get(key, default) Load YML file with configurations :: def load_yaml_config(yaml_path: Path) -> dict: with yaml_path.open("r", encoding="utf-8") as f: return yaml.safe_load(f) Normalize newlines and split on blank lines :: def split_paragraphs(text: str) -> list[str]: text = text.replace("\r\n", "\n").replace("\r", "\n").strip() if not text: return [] parts = re.split(r"\n\s*\n+", text) return [p.strip() for p in parts] def read_text(path: Path) -> str: # Print current time in hh:mm:ss format current_time = datetime.now().strftime("%H:%M:%S") # print(f"[{current_time}] Reading {path}") return path.read_text(encoding="utf-8") Main :: load_session_state() cfg = load_yaml_config(Path(config_file)) obsidian_path = Path(cfg["book"]) left_name = cfg["left"] right_name = cfg["right"] left_path = obsidian_path / left_name right_path = obsidian_path / right_name Check if paths to original Obsidian files exist :: if not left_path.exists(): st.error(f"Left file not found: {left_path}") st.stop() if not right_path.exists(): st.error(f"Right file not found: {right_path}") st.stop() Check if there are markdown files with the same names in current folder. If yes, then use them :: if os.path.exists(left_name): left_path = Path(left_name) if os.path.exists(right_name): right_path = Path(right_name) Load markdown files :: left_text = read_text(left_path) right_text = read_text(right_path) left_pars = split_paragraphs(left_text) right_pars = split_paragraphs(right_text) ptr = st.session_state.ptr max_page = max(len(left_pars), len(right_pars)) // page_len Page transitions :: def prev_page(): if st.session_state.page > 0: st.session_state.page -= 1 st.session_state.ptr = page_len - 1 save_session_state() st.rerun() def next_page(): if st.session_state.page < max_page-1: st.session_state.page += 1 st.session_state.ptr = 0 save_session_state() st.rerun() def prev_par(): if st.session_state.ptr > 0: st.session_state.ptr -= 1 save_session_state() st.rerun() else: prev_page() def next_par(): if st.session_state.ptr < page_len-1: st.session_state.ptr += 1 save_session_state() st.rerun() else: next_page() Top row of buttons :: col_1, col_2, col_3 = st.columns(3) with col_1: if st.button("Prev", width="stretch", icon=":material/step_out:"): prev_par() with col_2: if st.button("Next", width="stretch", icon=":material/step_into:"): next_par() with col_3: style = "text-align: right; font-weight: bold; font-size: small; text-decoration: underline; margin-right: 20px;" st.html(f"
Page {st.session_state.page+1}
") Show 2 columns of bilingua text :: page_start = st.session_state.page * page_len for i in range(page_start, page_start + page_len): col_left, col_right = st.columns(2) lp = left_pars[i] if i < len(left_pars) else "" rp = right_pars[i] if i < len(right_pars) else "" with col_left: if i==ptr+page_start: st.session_state.left_text = st.text_area("lp", lp, height="content", label_visibility="hidden") else: st.markdown(lp) with col_right: if i==ptr+page_start: st.session_state.right_text = st.text_area("rp", rp, height="content", label_visibility="hidden") else: st.markdown("> " + rp) Bottom row of buttons :: st.divider() col_1, col_2, col_3 = st.columns(3) with col_1: if st.button("Prev Page", width="stretch", icon=":material/step_out:"): prev_page() with col_2: if st.button("Next Page", width="stretch", icon=":material/step_into:"): next_page() with col_3: if st.button("Save", type="primary", width="stretch"): # Save left_pars to left_name file left_pars[ptr+page_start] = st.session_state.left_text with open(left_name, "w", encoding="utf-8") as f: f.write("\n\n".join(left_pars)) # Save right_pars to right_name file right_pars[ptr+page_start] = st.session_state.right_text with open(right_name, "w", encoding="utf-8") as f: f.write("\n\n".join(right_pars)) st.rerun()