Use your Kindle Paperwhite as a writing device
import piwrite.server as server

server.start()

piwrite/__main__.py
rberenguel/PiWrite
from typing import List, Union

from prompt_toolkit.keys import Keys

from piwrite.cursor import Cursor
from piwrite.line import Line


class Buffer:
    # TODO: move insertion to Line
    def __init__(self, lines: Union[List[Line], None] = None):
        if lines:
            self.lines = lines
        else:
            self.lines = list()

    def copy(self):
        return Buffer([line.copy() for line in self.lines])

    def __len__(self):
        return len(self.lines)

    def __getitem__(self, key: int):
        return self.lines[key]

    def __setitem__(self, key: int, value: Line):
        self.lines[key] = value

    def __repr__(self):
        joined = "|".join([str(l) for l in self.lines])
        return f"Buffer({joined})"

    def insert(self, key, cursor: Cursor):
        col: int = cursor.column

        if key == Keys.ControlM:
            current_line: int = cursor.line
            head = self.lines[current_line][0 : cursor.column]
            tail = self.lines[current_line][cursor.column :]
            # Keep indentation
            prev = str(self.lines[current_line])
            indent = len(prev) - len(prev.lstrip())
            self.lines[current_line] = Line(head)
            if len(tail) == 0 and indent > 0:
                self.lines.insert(current_line + 1, Line(" " * indent))
                cursor.to(current_line + 1, indent)
            else:
                self.lines.insert(current_line + 1, Line(tail))
                cursor.to(current_line + 1, 0)
            return

        if cursor.line + 1 > len(self.lines):
            self.lines.append(Line())
        self.lines[cursor.line].insert(col, key)
        cursor += 1

    def clip(self, cursor: Cursor):
        """Clip the cursor to the current line and buffer"""
        if cursor.line >= len(self):
            cursor.line = len(self) - 1
        if len(self) == 0:
            le = 0
        else:
            le = len(self[cursor.line])
        if cursor.column < 0:
            cursor.column = 0
        if cursor.line < 0:
            cursor.line = 0
        if le < cursor.column:
            cursor.column = le

    def delete(self, cursor: Cursor):
        col = cursor.column
        row = cursor.line
        if col == 0:
            if row == 0:
                return
            new_column = len(self.lines[row - 1])
            self.lines[row - 1] += self.lines[row]
            self.lines = self.lines[0:row] + self.lines[row + 1 :]
            cursor.line = row - 1
            cursor.column = new_column
            self.clip(cursor)
            return
        lin = self.lines[cursor.line]
        self.lines[cursor.line].delete(col)
        cursor -= 1

    def get(self):
        return self.lines

piwrite/buffer.py
rberenguel/PiWrite
from dataclasses import dataclass


@dataclass
class Cursor:
    line: int = 0
    column: int = 0

    def to(self, line=0, column=0):
        self.line = line
        self.column = column
        # The hidden column is used for clipping: when moving up
        # shorter/longer lines we should try to keep the longest
        self._column = column

    def __ixor__(self, inc: int):
        """Change column, because ^ looks like moving up/down"""
        self.line += inc
        return self

    def __iadd__(self, inc: int):
        """Change position, because += seems natural for columns"""
        self.column += inc
        return self

    def __isub__(self, inc: int):
        self.column -= inc
        return self

piwrite/cursor.py
rberenguel/PiWrite
import logging
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path

from prompt_toolkit.keys import Keys
from readability import Readability

from proselint import config as proselint_config
from proselint import tools as proselint_tools

from piwrite.buffer import Buffer
from piwrite.cursor import Cursor
from piwrite.line import Line
from piwrite.markdownify import markdownify
from piwrite.mode import Mode

logger = logging.getLogger("piwrite")


class Dispatcher:
    def __init__(self, editor=None):
        self.editor = editor
        self.editor._history = [Buffer()]
        self.editor.buffer = Buffer(lines=None)
        self.editor.cursor = Cursor(0, 0)

    def dispatch_command(self, command):
        # TODO: A Trie for this would work sooo much better than doing it by hand
        logger.info("Dispatching %s", str(command))
        if command == ["i"]:
            self.editor.clear_command()
            self.editor._mode = Mode.INSERT
            self.editor.updating_fields.add("mode")
            self.editor.cursor -= 1  # Seems to be giving problems!?
            self.editor.buffer.clip(self.editor.cursor)
            return
        if command[-1] == Keys.Escape:
            # TODO: this has no test
            self.editor.clear_command()
            return
        if command == ["v"]:
            lines = [str(lin) for lin in self.editor.buffer.get()]
            self.editor.visual = markdownify(lines, visible=False)
            self.editor.updating_fields.add("visual")
            return
        if command[-1] == Keys.ControlH:
            self.editor._command = self.editor._command[0:-2]
            self.editor.completions = None
            self.editor.completions_markdownified = (
                None  # TODO: wrap these two in a function
            )
            self.editor.updating_fields.add("command")
            self.editor.updating_fields.add("completions")
            return
        if command[-1] in self.editor.GENERIC_MOVEMENT:
            self.editor.status = (
                "I didn't bother implementing arrows or C-a/C-e here, sorry"
            )
            self.editor.updating_fields.add("status")
            self.editor._command.pop()
            self.editor.updating_fields.add("command")
            return
        if command == ["d"] or command == ["d", "a"] or command == ["d", "i"]:
            return
        if command == ["c"] or command == ["c", "a"] or command == ["c", "i"]:
            return
        # TODO: the commands below need tests
        if command == ["a"]:
            self.editor.clear_command()
            self.editor._mode = Mode.INSERT
            self.editor.updating_fields.add("mode")
            return
        if command == ["I"]:
            self.editor.clear_command()
            self.editor._mode = Mode.INSERT
            self.editor.updating_fields.add("mode")
            self.editor.cursor.to(column=0, line=self.editor.cursor.line)
            return
        if command == ["A"]:
            self.editor.clear_command()
            self.editor._mode = Mode.INSERT
            self.editor.updating_fields.add("mode")
            lin = self.editor.cursor.line
            end = len(self.editor.buffer[lin])
            self.editor.cursor.to(column=end, line=lin)
            return

        if command == ["o"]:
            self.editor.clear_command()
            self.editor._mode = Mode.INSERT
            self.editor.updating_fields.add("mode")
            lin = self.editor.cursor.line
            self.editor.buffer.lines.insert(lin + 1, Line())
            self.editor.cursor.to(column=0, line=lin + 1)
            return

        if command == ["u"]:
            # Undo-ish
            self.editor.clear_command()
            self.editor._history_pointer -= 1
            if self.editor._history_pointer < 0:
                logger.info("No further undo")
                self.editor.status = "No further undo information"
                self.editor._history_pointer = 0
            logger.debug("Undoing at %s", self.editor._history_pointer)
            self.editor.buffer = self.editor._history[
                self.editor._history_pointer
            ].copy()
            logger.debug("Buffer now: %s", self.editor.buffer)
            self.editor.buffer.clip(self.editor.cursor)
            return

        if command == [Keys.ControlR]:
            # Redo-ish
            self.editor.clear_command()
            self.editor._history_pointer += (
                1  # We need to jump "pressed escape" and "current"
            )
            if self.editor._history_pointer >= len(self.editor._history):
                self.editor.status = "No further redo information"
                self.editor._history_pointer = len(self.editor._history) - 1
            self.editor.buffer = self.editor._history[
                self.editor._history_pointer
            ].copy()
            self.editor.buffer.clip(self.editor.cursor)
            return

        if command == ["g"]:
            return

        if command == ["g", "g"]:
            self.editor.clear_command()
            self.editor.cursor.to(0, 0)
            return

        if command == ["G"]:
            self.editor.clear_command()
            self.editor.cursor.to(len(self.editor.buffer.lines) - 1, 0)
            return

        if command == ["p"]:
            self.editor.clear_command()
            paste = ["a"] + self.editor.yank + [Keys.Escape]
            self.editor.send(paste)
            return

        if command == ["P"]:
            self.editor.clear_command()
            paste = ["i"] + self.editor.yank + [Keys.Escape]
            self.editor.send(paste)
            return

        if command == ["c", "a", "w"]:
            self.editor.clear_command()
            self.editor.send(["dawa"])
        if command == ["c", "i", "w"]:
            self.editor.clear_command()
            self.editor.send(["diwa"])
        if command == ["d", "d"]:
            self.editor.clear_command()
            lin = self.editor.cursor.line
            self.editor.yank = [self.editor.buffer.lines[lin], Keys.ControlM]
            del self.editor.buffer.lines[lin]
            if len(self.editor.buffer) == 0:
                self.editor.buffer = Buffer()
            self.editor.buffer.clip(self.editor.cursor)
            return
        # TODO: clear repetition between daw and diw
        if command == ["d", "a", "w"]:
            self.editor.clear_command()
            row = self.editor.cursor.line
            line = self.editor.buffer[row]
            line.cursor = self.editor.cursor  # This is somewhat ugly
            new_line, word, col = line._aw()
            self.editor.yank = [word]
            self.editor.buffer[self.editor.cursor.line] = new_line
            self.editor.cursor.column = col
            self.editor.buffer.clip(self.editor.cursor)
            return

        if command == ["d", "i", "w"]:
            self.editor.clear_command()
            row = self.editor.cursor.line
            line = self.editor.buffer[row]
            line.cursor = self.editor.cursor  # This is somewhat ugly
            new_line, word, col = line._iw()
            self.editor.yank = [word]
            self.editor.buffer[self.editor.cursor.line] = new_line
            self.editor.cursor.column = col
            self.editor.buffer.clip(self.editor.cursor)
            return

        if command[0] == "q" and self.editor.previous_file is not None:
            cmd = [":E ", self.editor.previous_file[0], Keys.ControlM]
            self.editor.clear_command()
            self.editor.send(cmd)
            self.editor.dot = "nope"
            self.editor.updating_fields.add("dot")
            self.editor.filename = self.editor.previous_file[1]
            self.editor.updating_fields.add("filename")
            Path(self.editor.previous_file[0]).unlink()
            self.editor.previous_file = None

        if command[0] == ":" and (
            command[-1] != Keys.ControlM and command[-1] != Keys.ControlI
        ):
            logger.debug("Ignoring because no tab, no return")
            self.editor.completions = None
            self.editor.completions_markdownified = (
                None  # TODO: wrap these two in a function
            )
            self.editor.updating_fields.add("completions")
            return
        if command == [":", "k", "e", "y", "s", Keys.ControlM]:
            self.editor.clear_command()
            self.editor.log_keys = True
            self.editor.status = "Logging keys to buffer"
            self.editor.updating_fields.add("status")
            return
        if command == [":", "s", "t", "a", "t", "s", Keys.ControlM]:
            self.editor.clear_command()
            content = " ".join([str(lin) for lin in self.editor.buffer.get()])
            content = (
                content.replace("*", " ")
                .replace("_", " ")
                .replace("#", " ")
                .replace(":", " ")
            )
            words = len(content.split(" "))
            pars = len(
                [1 for lin in self.editor.buffer.get() if len(str(lin).strip()) > 0]
            )
            r = Readability(content)
            fc_line = ""
            f_line = ""
            w_line = f"<b>Stats and readability</b><br/>&nbsp; word count: {words}<br/>&nbsp; paragraphs: {pars}"
            try:
                fc = r.flesch_kincaid()
                f = r.flesch()
                fc_line = f"<b>Flesch-Kincaid</b><br/>&nbsp; score: {fc.score:.2f}<br/>&nbsp; grade: {fc.grade_level} (1-18)"
                f_line = f"<b>Flesch ease</b><br/>&nbsp; ease: {f.ease} ({f.score:.2f})"
            except Exception as e:
                f_line = f"Readability failure: {e}"
            finally:
                modal = "<br/>".join([w_line, fc_line, f_line])
                self.editor.modal = modal
                self.editor.updating_fields.add("modal")
            return

        if command == [":", "l", "i", "n", "t", Keys.ControlM]:
            self.editor.clear_command()
            _, tmpname = tempfile.mkstemp()
            resolved = Path(tmpname).resolve()
            previous_file = self.editor.filename
            previous_save_status = self.editor.saved
            cmd = [":W ", str(resolved), Keys.ControlM]
            self.editor.send(cmd)
            text = resolved.read_text()
            text = (
                text.replace("*", " ")
                .replace("_", " ")
                .replace("#", " ")
                .replace(":", " ")
            )
            p_suggestions = proselint_tools.lint(text, config=proselint_config.default)
            suggestions = [f"At {sug[2]}:{sug[3]}: {sug[1]}" for sug in p_suggestions]
            if len(suggestions) == 0:
                self.editor.modal = "No suggestions: As good as <i>The Great Gatsby</i>"
            else:
                self.editor.modal = "<br>".join(suggestions)
            self.editor.filename = previous_file
            self.editor.saved = previous_save_status
            self.editor.updating_fields.add("modal")
            self.editor.updating_fields.add("filename")
            self.editor.updating_fields.add("saved")
            return

        if command == [":", "q", Keys.ControlM]:
            self.editor.clear_command()
            if self.editor.saved:
                self.editor.status = "Shutting down" # This will never show, blocking
                time.sleep(1)  # I want this real blocking here
                subprocess.call(["shutdown", "-h", "now"])
            else:
                self.editor.status = "You have unsaved changes"
        if command == [":", "q", "!", Keys.ControlM]:
            self.editor.clear_command()
            self.editor.saved = True
            self.editor.send([":q", Keys.ControlM])
            return
        if command == [":", "h", Keys.ControlM]:
            self.editor.clear_command()
            _, tmpname = tempfile.mkstemp()
            resolved = str(Path(tmpname).resolve())
            self.editor.previous_file = (
                resolved,
                self.editor.filename,
            )  # Keep track of the previous "real" file (if any)
            cmd = [":W ", resolved, Keys.ControlM]
            self.editor.send(cmd)
            this_path = Path(__file__).resolve()
            root_dir = this_path.parent
            help = root_dir / "help"
            cmd = [":E ", str(help), Keys.ControlM]
            self.editor.send(cmd)
            return

        if command == [":", "d", "o", "t", Keys.ControlM]:
            # Render graphviz
            self.editor.clear_command()
            _, tmpname = tempfile.mkstemp()
            resolved = Path(tmpname).resolve()
            self.editor.previous_file = (
                str(resolved),
                self.editor.filename,
            )  # Keep track of the previous "real" file (if any)
            cmd = [":W ", str(resolved), Keys.ControlM]
            self.editor.send(cmd)
            img_resolved = (
                str((self.editor.docs / Path("imgs") / Path("graph")).resolve())
                + ".png"
            )
            template = (self.editor.docs / Path("dot_template.dot")).read_text()
            content = resolved.read_text()
            adapted = resolved.with_suffix(".dot")
            with adapted.open("a") as f:
                f.write(template)
                f.write(content)
                f.write("}")
            subprocess.call(["dot", "-Tpng", str(adapted), "-o", img_resolved])
            self.editor.status = img_resolved
            self.editor.updating_fields.add("status")
            self.editor.dot = "/docs/imgs/graph.png"
            self.editor.updating_fields.add("dot")
            return

        if command[0:2] == [":", "w"] and command[-1] == Keys.ControlM:
            # Write file
            filename = "".join(command[3:-1])
            self.editor.clear_command()
            if filename.strip() == "":
                filename = self.editor.filename
                self.editor.updating_fields.add("filename")
            try:
                (self.editor.docs / filename).write_text(
                    "\n".join([str(lin) for lin in self.editor.buffer.get()])
                )
                self.editor.filename = filename
                self.editor.updating_fields.add("filename")
                self.editor.saved = True
                self.editor.updating_fields.add("saved")
                self.editor.status = f"Saved as {filename}"
                self.editor.updating_fields.add("status")
            except Exception as e:
                self.editor.err = str(e)
                self.editor.updating_fields.add("err")
            return
        if command[0:2] == [":", "W"] and command[-1] == Keys.ControlM:
            # This should be protected like E
            filename = "".join(command[3:-1])
            self.editor.clear_command()
            try:
                Path(filename).write_text(
                    "\n".join([str(lin) for lin in self.editor.buffer.get()])
                )
                self.editor.filename = filename
                self.editor.saved = True
                self.editor.status = f"Special saved as {filename}"
                self.editor.updating_fields.add("filename")
                self.editor.updating_fields.add("saved")
                self.editor.updating_fields.add("status")
            except Exception as e:
                self.editor.err = str(e)
                self.editor.updating_fields.add("err")
            return
        if "".join(command[0:4]) == ":rot" and command[-1] == Keys.ControlM:
            self.editor.clear_command()
            if self.editor.rot == "0":
                self.editor.rot = "90"
            else:
                self.editor.rot = "0"
            self.editor.updating_fields.add("rot")
            return
        if "".join(command[0:5]) == ":mono" and command[-1] == Keys.ControlM:
            self.editor.clear_command()
            self.editor.font = "mono"
            self.editor.updating_fields.add("font")
            return
        if "".join(command[0:5]) == ":sans" and command[-1] == Keys.ControlM:
            self.editor.clear_command()
            self.editor.font = "sans"
            self.editor.updating_fields.add("font")
            return
        if "".join(command[0:6]) == ":serif" and command[-1] == Keys.ControlM:
            self.editor.clear_command()
            self.editor.font = "serif"
            self.editor.updating_fields.add("font")
            return
        if "".join(command[0:6]) == ":latex" and command[-1] == Keys.ControlM:
            self.editor.clear_command()
            self.editor.font = "latex"
            self.editor.updating_fields.add("font")
            return
        if "".join(command[0:9]) == ":fontsize" and command[-1] == Keys.ControlM:
            self.editor.fontsize = int("".join(command[10:-1]).strip())
            self.editor.clear_command()
            self.editor.status = f"Set font size to {self.editor.fontsize}"
            self.editor.updating_fields.add("fontsize")
            self.editor.updating_fields.add("status")
            return
        if "".join(command[0:3]) == ":fs" and command[-1] == Keys.ControlM:
            self.editor.fontsize = int("".join(command[4:-1]).strip())
            self.editor.clear_command()
            self.editor.status = f"Set font size to {self.editor.fontsize}"
            self.editor.updating_fields.add("fontsize")
            self.editor.updating_fields.add("status")
            return
        if (command[0:2] == [":", "e"] or command[0:3] == [":", "e", "!"]) and command[
            -1
        ] == Keys.ControlI:
            command.pop()  # Drop the tab
            if self.editor.completions is None:
                filename = "".join(command).replace(":e!", "").replace(":e", "").strip()
                logger.debug("Globbing on %s", filename)
                files = [str(f.name) for f in self.editor.docs.glob(filename + "*")]
                if len(files) == 0:
                    self.editor.completions = None
                else:
                    self.editor.completions = {"files": files, "idx": -1}
                self.editor.updating_fields.add("completions")
            else:
                self.editor.completions["idx"] = (
                    self.editor.completions["idx"] + 1
                ) % len(self.editor.completions["files"])
                # TODO: In addition to this, deleting or writing will need to clear completions, reset index…
                md = []
                for i, completion in enumerate(self.editor.completions["files"]):
                    if i == self.editor.completions["idx"]:
                        md.append(f"::{completion}::")
                    else:
                        md.append(completion)
                self.editor.completions_markdownified = markdownify(
                    [" ".join(md)], visible=False
                )
                self.editor.updating_fields.add("completions")
            return
        if command[0:4] == [":", "v", "i", "z"] and command[-1] == Keys.ControlM:
            try:
                viz_val = "".join(command[4:-1]).strip()
                self.editor.clear_command()
                if len(viz_val) == 0:
                    self.editor.status = "Clearing custom viz"
                    self.editor.viz = None
                else:
                    viz, shift = viz_val.split(":")
                    viz = int(viz)
                    shift = int(shift)
                    self.editor.viz = (viz, shift)
            except Exception as e:
                self.editor.clear_command()
                self.editor.status = f"viz has to be of the form int:int or empty ({e})"
                self.editor.updating_fields.add("status")
            self.editor.status = f"Setting shift to {self.editor.viz}"
            self.editor.updating_fields.add("status")
            return
        if command[0:3] == [":", "e", "!"] and command[-1] == Keys.ControlM:
            if self.editor.completions is None:
                filename = "".join(command[4:-1])
            else:
                filename = self.editor.completions["files"][
                    self.editor.completions["idx"]
                ]
            self.editor.clear_command()
            self.editor.status = ""
            self.editor.saved = True
            self.editor.updating_fields.add("saved")
            self.editor.updating_fields.add("status")
            self.editor.send([":e ", filename, Keys.ControlM])
            return
        if command[0:2] == [":", "e"] and command[-1] == Keys.ControlM:
            if not self.editor.saved:
                self.editor.status = "You have unsaved changes"
                self.editor.updating_fields.add("saved")
                return
            if self.editor.completions is None:
                filename = "".join(command[3:-1])
            else:
                filename = self.editor.completions["files"][
                    self.editor.completions["idx"]
                ]
            self.editor.clear_command()
            try:
                text = (self.editor.docs / filename).read_text()
                lines = text.split("\n")
                new_buffer = Buffer(lines=[Line(line) for line in lines])
                self.editor.buffer = new_buffer
                self.editor.filename = filename
                self.editor.saved = True
                self.editor.status = f"Loaded {self.editor.filename}"
                self.editor.updating_fields.add("status")
                self.editor.updating_fields.add("saved")
                self.editor.updating_fields.add("filename")
            except Exception as e:
                self.editor.err = str(e)
                self.editor.updating_fields.add("err")
            self.editor.cursor.to(0, 0)
            if self.editor.filename.endswith(".dot"):
                self.editor.send([":mono", Keys.ControlM])
            return
        if command[0:2] == [":", "E"] and command[-1] == Keys.ControlM:
            if self.editor.completions is None:
                filename = "".join(command[3:-1])
            else:
                filename = self.editor.completions["files"][
                    self.editor.completions["idx"]
                ]
            self.editor.clear_command()
            try:
                logger.debug("Opening %s", filename)
                text = Path(filename).read_text()
                lines = text.split("\n")
                self.editor.buffer = Buffer([Line(line) for line in lines])
                self.editor.filename = Path(filename).name
                self.editor.saved = True
                self.editor.status = f"Loaded {self.editor.filename}"
                self.editor.updating_fields.add("status")
                self.editor.updating_fields.add("saved")
                self.editor.updating_fields.add("filename")
            except Exception as e:
                self.editor.err = str(e)
                self.editor.updating_fields.add("err")
            self.editor.cursor.to(0, 0)
            return
        self.editor.clear_command()

piwrite/dispatcher.py
rberenguel/PiWrite
import logging
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass
from enum import Enum
from pathlib import Path

from prompt_toolkit.key_binding.key_processor import KeyPress
from prompt_toolkit.keys import Keys
from proselint import config as proselint_config
from proselint import tools as proselint_tools

from piwrite.dispatcher import Dispatcher
from piwrite.line import Line
from piwrite.markdownify import markdownify
from piwrite.mode import Mode

logger = logging.getLogger("piwrite")


class Editor:
    UNDO_DEPTH = 10

    def __init__(self):
        # TODO: Some could be properties
        # TODO: Some should be saved and restored on quit
        self.refresh = False
        self._history_pointer = 0
        self._mode = Mode.NORMAL
        self._command = []
        self.yank = [""]
        self.updating_fields = set()
        self.viz = None
        self.setup_movement()
        self.rot = "0"
        self.dot = "nope"
        self.log_keys = False
        self.filename = "unnamed"
        self.previous_file = None
        self.saved = False
        self.modal = ""
        self.visual = ""
        self.status = None
        self.completions = None
        self.completions_markdownified = None
        self.font = "serif"
        self.fontsize = 12
        self.err = None
        home = Path.home()
        self.docs = home / "piwrite-docs/"
        self.docs.mkdir(exist_ok=True)
        (self.docs / Path("imgs")).mkdir(exist_ok=True)
        self.dispatcher = Dispatcher(editor=self)

    def send(self, arr):
        """Send an array containing strings and keys to be parsed"""
        logger.info(f"Command being sent: {arr}")
        for thing in arr:
            if isinstance(thing, Enum):
                self.dispatch(KeyPress(thing))
            else:
                for let in thing:
                    self.dispatch(KeyPress(let))

    def mode(self):
        return str(self._mode)

    def get(self):
        # TODO: needs a test
        lines = [str(lin) for lin in self.buffer.get()]  # "cheap" copy
        if self.cursor.line + 1 > len(lines):
            lin = ""
            lines.append(lin)
        lin = lines[self.cursor.line]
        col = self.cursor.column
        # if col == 0:
        #    col = 1
        if len(lin) > col:
            end = lin[col:]
        else:
            end = ""
        if col <= 0:
            letter = " "
        else:
            letter = lin[col - 1]
        if self._mode == Mode.INSERT:
            if False:  # col == 1:
                lines[self.cursor.line] = (
                    """<span id="ins0">""" + letter + """</span>""" + end
                )
            else:
                if col - 1 < 0:
                    start = ""
                else:
                    start = lin[0 : col - 1]
                lines[self.cursor.line] = (
                    start
                    + """<span id="caret" class="ins">"""
                    + letter
                    + """</span>"""
                    + end
                )
        if self._mode == Mode.NORMAL:
            if col - 1 < 0:
                start = ""
            else:
                start = lin[0 : col - 1]
            lines[self.cursor.line] = (
                start
                + """<span id="caret" class="normal">"""
                + letter
                + """</span>"""
                + end
            )
        if self.viz:
            viz = self.viz[0]
            shift = self.viz[1]
        else:
            viz = int(1100 / (2 * self.fontsize)) + 2  # _very_ rough approx
            shift = int(viz / 2)
            if self.rot == "90":  # TODO: Convert these to an Enum
                viz = int(int(self.fontsize) / 9)
                shift = 2
        row = self.cursor.line
        if row < viz:
            return markdownify(lines, row)
        else:
            return markdownify(lines[row - shift :], shift)

    def setup_movement(self):
        def up():
            self.cursor ^= -1
            self.buffer.clip(self.cursor)
            return

        def down():
            self.cursor ^= +1
            self.buffer.clip(self.cursor)
            return

        def right():
            self.cursor += 1
            self.buffer.clip(self.cursor)
            return

        def left():
            self.cursor -= 1
            self.buffer.clip(self.cursor)
            return

        self.GENERIC_MOVEMENT = {
            Keys.ControlA: lambda: self.cursor.to(line=self.cursor.line, column=0),
            Keys.ControlE: lambda: self.cursor.to(
                line=self.cursor.line, column=len(self.buffer.get()[self.cursor.line])
            ),
            Keys.Up: up,
            Keys.Down: down,
            Keys.Left: left,
            Keys.Right: right,
        }

    def dispatch(self, _key):
        if _key is None:
            return
        key = _key.key
        if key == Keys.ControlC:
            sys.exit(0)
        if key == Keys.Escape and self.log_keys:
            self.log_keys = False
            return
        if self.log_keys:
            # TODO: move this appending to be a method on buffer
            self.buffer.lines.append(Line(str(key)))
            return

        if key == Keys.ControlP:
            p_suggestions = proselint_tools.lint(
                str(self.buffer.lines[self.cursor.line]),
                config=proselint_config.default,
            )
            suggestions = [f"At {sug[3]}: {sug[1]}" for sug in p_suggestions]
            self.modal = "<br>".join(suggestions)
            self.updating_fields.add("modal")
            return

        if key in self.GENERIC_MOVEMENT:
            self.GENERIC_MOVEMENT[key]()
            return

        if key == Keys.ControlQ:
            self.refresh = True
            self.clear_command()
            return

        if key == Keys.Escape and self.modal != "":
            logger.info("Hiding modal")
            self.modal = ""
            self.updating_fields.add("modal")
            self.clear_command()
            return

        if key == "q" and self.visual != "":
            logger.info("Hiding visual")
            self.visual = ""
            self.updating_fields.add("visual")
            self.clear_command()
            return

        if self._mode == Mode.INSERT:
            if key == Keys.Escape:
                self._mode = Mode.NORMAL
                self.updating_fields.add("mode")
                self.cursor -= 1  # This seems to be the vim behaviour
                self.buffer.clip(self.cursor)
                logger.debug(
                    "History before: %s, %s", self._history, self._history_pointer
                )
                # TODO: All this needs better testing, and it may get tricky
                if self._history_pointer + 1 >= len(self._history):
                    logger.debug("Adding at the end")
                    self._history.append(self.buffer.copy())
                else:
                    self._history[self._history_pointer + 1] = self.buffer.copy()
                self._history_pointer += 1
                self._history_pointer = min(self._history_pointer, self.UNDO_DEPTH - 1)
                logger.debug("Clipping pointer at %s", self._history_pointer)
                if len(self._history) > self.UNDO_DEPTH:
                    self._history = self._history[-self.UNDO_DEPTH :]
                    assert len(self._history) == self.UNDO_DEPTH
                logger.debug(
                    "History after: %s, %s", self._history, self._history_pointer
                )
                return
            if self.saved:
                self.updating_fields.add("saved")
            self.saved = False
            if key == Keys.Delete or key == Keys.ControlH:
                self.buffer.delete(self.cursor)
                return
            self.buffer.insert(key, self.cursor)

        if self._mode == Mode.NORMAL:
            self._command.append(key)
            self.updating_fields.add("command")
            self.dispatch_command(self._command)
        return

    def clear_command(self):
        self.completions = None
        self.completions_markdownified = None
        self.updating_fields.add("completions")
        self.status = "&nbsp;"
        self.updating_fields.add("status")
        self._command = []
        self.updating_fields.add("command")

    def command(self):
        filt = [str(l) for l in self._command if len(str(l)) == 1]
        return "".join(filt)

    def dispatch_command(self, command):
        self.dispatcher.dispatch_command(command)

piwrite/editor.py
rberenguel/PiWrite
class Line:
    def __init__(self, contents=None):
        if contents:
            self.contents = contents
        else:
            self.contents = ""
        self.cursor = None

    def copy(self):
        return Line(self.contents)

    def __getitem__(self, key):
        return self.contents[key]

    def __len__(self):
        return len(self.contents)

    def __repr__(self):
        return self.contents

    def __eq__(self, other):
        if isinstance(other, Line):
            return self.contents == other.contents
        if isinstance(other, str):
            return self.contents == other

    def __iadd__(self, other):
        self.contents += other.contents
        return self

    def delete(self, column):
        s = self.contents
        self.contents = s[0 : column - 1] + s[column:]

    def insert(self, column, letter):
        s = self.contents
        if len(self.contents) == column:
            self.contents = s + letter
        else:
            # TODO: keep the breakage point and keep inserting until a change of cursor
            # Probably needs to keep track of cursor, which I haven't used yet.
            self.contents = (
                s[0:column] + letter + s[column:]
            )  # Wasteful, but ok for a PoC

    def _iw(self):
        """Handle, kind of, the inside word text object. Returns the new cursor position"""
        # This is uglily hardcoded like this
        # Note that daw is diw + deleting a space
        col = self.cursor.column
        # For now will ignore any other separators (vim handles . and others)
        lin = self.contents
        end = lin.find(" ", col)
        start = lin.rfind(" ", 0, col)
        if end == -1 and start == -1:
            return Line(""), "", 0
        if end == -1:
            return Line(lin[0 : start + 1]), lin[start + 1 :], start + 1
        if start == -1:
            return Line(lin[end:]), lin[0:end], 0
        return Line(lin[0 : start + 1] + lin[end:]), lin[start + 1 : end], start + 1

    def _aw(self):
        """Handle, kind of, the around word text object. Returns the new cursor position"""
        # This is uglily hardcoded like this
        # Note that daw is diw + deleting a space
        col = self.cursor.column
        # For now will ignore any other separators (vim handles . and others)
        lin = self.contents
        end = lin.find(" ", col)
        start = lin.rfind(" ", 0, col)
        if end == -1 and start == -1:
            print("Deletion has been empty-ish")
            return Line(""), lin, 0
        if end == -1:
            return Line(lin[0:start]), lin[start:], start + 1
        if start == -1:
            return Line(lin[end + 1 :]), lin[0 : end + 1], 0
        return Line(lin[0:start] + lin[end:]), lin[start + 1 : end + 1], start + 1

piwrite/line.py
rberenguel/PiWrite
import re


def start_highlighter(ent, clip=0):
    def focuser(line, idx, current):
        if idx == current:
            return f"""<{ent} class="focus">{line[clip:]}</{ent}>"""
        else:
            return f"""<{ent}>{line[clip:]}</{ent}>"""

    return focuser


STARTS = {
    "# ": start_highlighter("h1"),  # lambda t: f"<h1>{t}</h1>",
    "## ": start_highlighter("h2"),  # lambda t: f"<h2>{t}</h2>",
    "### ": start_highlighter("h3"),  # lambda t: f"<h3>{t}</h3>",
    "#### ": start_highlighter("h4"),  # lambda t: f"<h4>{t}</h4>",
    "- ": start_highlighter("li", clip=2),  # lambda t: f"<li>{t[2:]}</li>",
}


def bolding(line, visible=True):
    beg_of_word = re.compile(r"(^|\s)\*\*(\S)")
    end_of_word = re.compile(r"(\S)\*\*($|\s|:|\.)")
    if visible:
        mark = "**"
    else:
        mark = ""
    new_line = re.sub(beg_of_word, f"\\1<b>{mark}\\2", line)
    new_line = re.sub(end_of_word, f"\\1{mark}</b>\\2", new_line)
    return new_line


def highlighting(line, visible=True):
    beg_of_word = re.compile(r"(^|\s)::(\S)")
    end_of_word = re.compile(r"(\S)::($|\s)")
    if visible:
        mark = "::"
    else:
        mark = ""
    new_line = re.sub(beg_of_word, f"\\1<span class='highlight'>{mark}\\2", line)
    new_line = re.sub(end_of_word, f"\\1{mark}</span>\\2", new_line)
    return new_line


def italicising(line, visible=True):
    beg_of_word = re.compile(r"(^|\s)_(\S)")
    end_of_word = re.compile(r"(\S)_($|\s|:|\.)")
    if visible:
        mark = "_"
    else:
        mark = ""
    new_line = re.sub(beg_of_word, f"\\1<i>{mark}\\2", line)
    new_line = re.sub(end_of_word, f"\\1{mark}</i>\\2", new_line)
    return new_line


def teletyping(line, visible=True):
    beg_of_word = re.compile(r"(^|\s)`(\S)")
    end_of_word = re.compile(r"(\S)`($|\s|:|\.)")
    if visible:
        mark = "`"
    else:
        mark = ""
    new_line = re.sub(beg_of_word, f"\\1<tt>{mark}\\2", line)
    new_line = re.sub(end_of_word, f"\\1{mark}</tt>\\2", new_line)
    return new_line


def focus(line, idx, current):
    if idx == current:
        return f"""<span class="focus">{line}</span>"""
    return line


def markdownify(original_lines, current_line=-1, visible=True):
    """Convert simple Markdown to reasonable HTML (with some visible Markdown markers), with highlighting of the current line"""
    new_lines = []
    skip = False
    for idx, line in enumerate(original_lines):
        newline = line
        if line == "---":
            new_lines.append(focus("<hr/>", idx, current_line))
            continue
        if newline == "":
            new_lines.append(
                focus("""<span class="small">&nbsp;</span>""", idx, current_line)
            )
            continue

        newline = bolding(newline, visible)
        newline = italicising(newline, visible)
        newline = teletyping(newline, visible)
        newline = highlighting(newline, visible)
        for key, transform in STARTS.items():
            if line.startswith(key):
                skip = True
                if key == "- ":
                    newline = transform(newline, idx, current_line)
                else:
                    newline = transform(newline, idx, current_line)
        else:
            if not skip:
                newline = focus(newline, idx, current_line)
                newline = newline + "<br/>"
            else:
                skip = False
        new_lines.append(newline)
    return new_lines

piwrite/markdownify.py
rberenguel/PiWrite
from enum import Enum


class Mode(Enum):
    NORMAL = "N"
    INSERT = "I"

piwrite/mode.py
rberenguel/PiWrite
import asyncio
import importlib
import logging
import os

try:
    import importlib.resources as pkg_resources
except ImportError:
    # I shouldn't have to do this
    import importlib_resources as pkg_resources

import aiohttp.web
import socketio
from colorlog import ColoredFormatter
from prompt_toolkit.input import create_input
from prompt_toolkit.keys import Keys

logger = logging.getLogger("piwrite")

import nltk

from piwrite.editor import Editor

HOST = os.getenv("PIWRITE_HOST", "127.0.0.1")
DEBUG = os.getenv("PIWRITE_DEBUG", "False") == "True"
INFO = os.getenv("PIWRITE_INFO", "False") == "True"
PORT = int(os.getenv("PIWRITE_PORT", 80))

STATIC_FOLDER = pkg_resources.files("piwrite") / "static"


def configure_logger():
    """Fancy logging is nicer"""
    formatter = ColoredFormatter(
        "%(log_color)s%(levelname)s - %(message)s",
        datefmt=None,
        reset=True,
        log_colors={
            "DEBUG": "yellow",
            "INFO": "cyan",
            "WARNING": "purple",
            "ERROR": "red",
            "CRITICAL": "red,bg_white",
        },
        secondary_log_colors={},
        style="%",
    )
    handler = logging.StreamHandler()
    handler.setFormatter(formatter)
    logger.addHandler(handler)


def staticHandle(file):
    async def handler(request):
        return aiohttp.web.FileResponse(file)

    return handler


v = Editor()

sio = socketio.AsyncServer(logger=False, engineio_logger=False, async_mode="aiohttp")


def init_map():
    update_only_map = {
        "saved": {"sent": False, "old": None, "exec": lambda: v.saved},
        "completions": {
            "sent": False,
            "old": None,
            "exec": lambda: v.completions_markdownified,
        },
        "mode": {"sent": False, "old": None, "exec": lambda: v.mode()},
        "err": {"sent": False, "old": None, "exec": lambda: v.err},
        "filename": {"sent": False, "old": None, "exec": lambda: v.filename},
        "command": {"sent": False, "old": None, "exec": lambda: v.command()},
        "modal": {"sent": False, "old": None, "exec": lambda: v.modal},
        "visual": {"sent": False, "old": None, "exec": lambda: v.visual},
        "status": {"sent": False, "old": None, "exec": lambda: v.status},
        "font": {"sent": False, "old": None, "exec": lambda: v.font},
        "fontsize": {"sent": False, "old": None, "exec": lambda: v.fontsize},
        "rot": {"sent": False, "old": None, "exec": lambda: v.rot},
        "dot": {"sent": False, "old": None, "exec": lambda: v.dot},
    }
    return update_only_map


async def the_loop():
    key = asyncio.Event()
    key_press = None
    done = asyncio.Event()
    inp = create_input()

    update_only_map = init_map()
    update_only_fields = list(update_only_map.keys())

    def keys_ready():
        nonlocal key_press
        for _key_press in inp.read_keys():
            key.set()
            key_press = _key_press
            if key_press.key == Keys.ControlC:
                done.set()

    with inp.raw_mode():
        with inp.attach(keys_ready):
            while True:
                await key.wait()
                logger.debug(key_press)
                v.dispatch(key_press)
                await sio.emit("buffer", {"data": v.get()})
                if v.refresh:
                    logger.info("Sending a full refresh")
                    for field, val in update_only_map.items():
                        await sio.emit(field, {"data": val["exec"]()})
                    v.refresh = False
                logger.info(f"Updating {len(v.updating_fields)} fields")
                for field in v.updating_fields:
                    new_val = update_only_map[field]["exec"]()
                    await sio.emit(field, {"data": new_val})
                v.updating_fields.clear()
                key.clear()


async def main():
    app = aiohttp.web.Application()
    app.router.add_route("GET", "/", staticHandle(STATIC_FOLDER / "index.html"))
    app.add_routes([aiohttp.web.static("/static", STATIC_FOLDER, show_index=False)])
    app.add_routes([aiohttp.web.static("/docs", v.docs, show_index=True)])
    sio.attach(app)
    runner = aiohttp.web.AppRunner(app)
    await runner.setup()
    site = aiohttp.web.TCPSite(runner, host=HOST, port=PORT)
    await site.start()
    logger.warning(f"Server started at '{HOST}:{PORT}'")
    await the_loop()
    await asyncio.Event().wait()


def start():
    configure_logger()
    if DEBUG:
        logger.setLevel(logging.DEBUG)
    elif INFO:
        logger.setLevel(logging.INFO)
    else:
        logger.setLevel(logging.WARNING)
    try:
        nltk.data.find("tokenizers/punkt")
    except LookupError:
        logger.warning("Punkt not available")
        try:
            # This means you need to run it at least
            # once without access point mode
            nltk.download("punkt")
        except:
            pass
    asyncio.run(main())


if __name__ == "__main__":
    start()

piwrite/server.py
rberenguel/PiWrite
from enum import Enum

import pytest
from prompt_toolkit.key_binding.key_processor import KeyPress
from prompt_toolkit.keys import Keys

import piwrite.editor as editor

LETTER_I = KeyPress("i")
ESC = Keys.Escape
DEL = Keys.ControlH
ENT = Keys.ControlM
LEFT = Keys.Left


def K(letter):
    return KeyPress(letter)


def test_insertion():
    v = editor.Editor()
    v.send(["if"])
    assert v.buffer.get()[0] == "f"


def test_save():
    v = editor.Editor()
    filename = "foo"
    text = "text"
    command = ["i", text, ESC, ":w ", filename, ENT]
    v.send(command)
    assert v.mode() == str(editor.Mode.NORMAL)
    docs = v.docs
    fil = docs / filename
    written_text = fil.read_text()
    assert written_text == text
    assert v.filename == filename
    fil.unlink()


def test_read():
    v = editor.Editor()
    filename = "foo"
    text = "text"
    command = ["i", text, ESC, ":w ", filename, ENT]
    v.send(command)
    w = editor.Editor()
    command2 = ["it", ESC, ":e ", filename, ENT]
    w.send(command)
    assert w.buffer.get()[0] == "text"
    docs = w.docs
    fil = docs / "foo"
    fil.unlink()


def test_left():
    v = editor.Editor()
    cmd = ["if"]
    v.send(cmd)
    assert v.buffer.get()[0] == "f"
    cmd = [LEFT, "g"]
    v.send(cmd)
    assert v.buffer.get()[0] == "gf"
    v.send([ESC, LEFT, "ih"])
    assert v.buffer.get()[0] == "hgf"


def test_right():
    v = editor.Editor()
    cmd = ["i2"]
    v.send(cmd)
    assert v.buffer.get()[0] == "2"
    cmd = [LEFT, "1", Keys.Right, "3"]
    v.send(cmd)
    assert v.buffer.get()[0] == "123"
    v.send([ESC, Keys.ControlA, Keys.Right, "i."])
    assert v.buffer.get()[0] == ".123"


def test_up():
    v = editor.Editor()
    cmd = ["i1"]
    v.send(cmd)
    assert v.buffer.get()[0] == "1"
    cmd = [ENT, "2"]
    v.send(cmd)
    assert v.buffer.get()[1] == "2"
    cmd = [Keys.Up, "3"]
    v.send(cmd)
    assert v.buffer.get()[0] == "13"
    cmd = [ENT, "4", ESC, Keys.Up]
    v.send(cmd)
    assert v.mode() == str(editor.Mode.NORMAL)
    cmd = ["i5"]
    v.send(cmd)
    assert (
        v.buffer.get()[0] == "513"
    )  # Remember ESC switches cursor one position back from insertion


def test_new_lines():
    v = editor.Editor()
    cmd = ["i1"]
    v.send(cmd)
    assert v.buffer.get()[0] == "1"
    cmd = [ENT, "2"]
    v.send(cmd)
    assert v.buffer.get() == ["1", "2"]


def test_line_break():
    v = editor.Editor()
    cmd = ["i12", LEFT, ENT]
    v.send(cmd)
    assert v.buffer.get() == ["1", "2"]


def test_indent_ish():
    v = editor.Editor()
    cmd = ["i  b", ENT, "a"]
    v.send(cmd)
    s = v.buffer.get()[1]
    assert v.buffer.get()[0] == "  b"
    assert s == "  a"


def test_deletion():
    v = editor.Editor()
    cmd = ["i123", DEL]
    v.send(cmd)
    assert v.buffer.get()[0] == "12"


def test_deletion_of_beginning():
    v = editor.Editor()
    cmd = ["i123", ESC, "o456", Keys.ControlA, DEL]
    v.send(cmd)
    assert v.buffer.get()[0] == "123456"
    assert len(v.buffer) == 1


def test_dd():
    v = editor.Editor()
    cmd = ["i123", ESC, "ddi1"]
    v.send(cmd)
    assert v.buffer.get()[0] == "1"

    v = editor.Editor()
    cmd = ["i123", ENT, "456", Keys.Up, ESC, "dd"]
    v.send(cmd)
    assert v.buffer.get()[0] == "456"


@pytest.mark.parametrize(
    "text,deleted,yanked",
    [
        (["a word", ESC], "a", " word"),
        (["a word ", LEFT, LEFT, ESC], "a ", "word "),
        (["a word ", LEFT, LEFT, ESC], "a ", "word "),
        (["a wo ", LEFT, LEFT, LEFT, LEFT, ESC], "wo ", "a "),
    ],
)
def test_delete_around_word(text, deleted, yanked):
    v = editor.Editor()
    cmd = ["i"] + text + ["daw"]
    v.send(cmd)
    assert str(v.buffer.get()[0]) == deleted
    assert v.yank == [yanked]


@pytest.mark.parametrize(
    "text,deleted,yanked",
    [
        (["a word", ESC], "a ", "word"),
        (["a word ", LEFT, LEFT, ESC], "a  ", "word"),
        (["a word ", LEFT, LEFT, ESC], "a  ", "word"),
        (["a wo ", LEFT, LEFT, LEFT, LEFT, ESC], " wo ", "a"),
    ],
)
def test_delete_inside_word(text, deleted, yanked):
    v = editor.Editor()
    cmd = ["i"] + text + ["diw"]
    v.send(cmd)
    assert str(v.buffer.get()[0]) == deleted
    assert v.yank == [yanked]


@pytest.mark.parametrize(
    "text,deleted",
    [
        (["da word", ESC], "dafoo"),
        (["da word ", LEFT, LEFT, LEFT, ESC], "da foo"),
        (["da word ", LEFT, LEFT, LEFT, ESC], "da foo"),
        (["da wo ", LEFT, LEFT, LEFT, LEFT, ESC], "foowo "),
    ],
)
def test_change_around_word(text, deleted):
    v = editor.Editor()
    cmd = ["i"] + text + ["cawfoo"]
    v.send(cmd)
    assert str(v.buffer.get()[0]) == deleted


@pytest.mark.parametrize(
    "text,deleted",
    [
        (["da word", ESC], "da foo"),
        (["da word ", LEFT, LEFT, LEFT, ESC], "da foo "),
        (["da word ", LEFT, LEFT, LEFT, ESC], "da foo "),
        (["da wo ", LEFT, LEFT, LEFT, LEFT, ESC], "foo wo "),
    ],
)
def test_change_inside_word(text, deleted):
    v = editor.Editor()
    cmd = ["i"] + text + ["ciwfoo"]
    v.send(cmd)
    assert str(v.buffer.get()[0]) == deleted


def test_paste_problem():
    v = editor.Editor()
    cmd = ["ihello", ESC, "dawa", ENT, ESC, "p"]
    v.send(cmd)
    assert str(v.buffer.get()[1]) == "hello"


def test_basic_undo():
    v = editor.Editor()
    cmd = ["i1 ", ESC, "A2 ", ESC, "A3 ", ESC, "uA4"]
    v.send(cmd)
    assert str(v.buffer.get()[0]) == "1 2 4"


def test_branch_undo():
    v = editor.Editor()
    cmd = ["i1 ", ESC, "A2 ", ESC, "A3 ", ESC, "uuA4 ", ESC, "A5"]
    v.send(cmd)
    assert str(v.buffer.get()[0]) == "1 4 5"


def test_redo():
    v = editor.Editor()
    cmd = ["i1 ", ESC, "A2 ", ESC, "A3 ", ESC, "uu", Keys.ControlR]
    v.send(cmd)
    assert str(v.buffer.get()[0]) == "1 2 "


def test_redo_and_undo():
    v = editor.Editor()
    # 1 2 3
    # 1
    # 1 2
    # 1 2 4
    # 1 2
    cmd = ["i1 ", ESC, "A2 ", ESC, "A3 ", ESC, "uu", Keys.ControlR, "A4", ESC, "u"]
    v.send(cmd)
    assert str(v.buffer.get()[0]) == "1 2 "


def test_undo_depth():
    v = editor.Editor()
    v.UNDO_DEPTH = 2
    cmd = ["i1 ", ESC, "A2 ", ESC, "A3 ", ESC, "uA4"]
    v.send(cmd)
    assert str(v.buffer.get()[0]) == "1 2 4"


def test_redo_and_undo_with_depth():
    v = editor.Editor()
    v.UNDO_DEPTH = 2
    # 1 2 3
    # 1 2 (just one depth)
    # 1 2 3 (redo)
    # 1 2 3 4
    # 1 2 3 (u)
    cmd = ["i1 ", ESC, "A2 ", ESC, "A3 ", ESC, "uu", Keys.ControlR, "A4", ESC, "u"]
    v.send(cmd)
    assert str(v.buffer.get()[0]) == "1 2 3 "


def test_redo_limit():
    v = editor.Editor()
    v.send([Keys.ControlR, Keys.ControlR])


def test_undo_limit():
    v = editor.Editor()
    v.send(["uuuu"])


def test_basic_paste():
    v = editor.Editor()
    cmd = ["ia word", ESC, "diwa", ENT, ESC, "p"]
    v.send(cmd)
    assert str(v.buffer.get()[1]) == "word"

test/test_editor.py
rberenguel/PiWrite
import pytest

from piwrite.markdownify import markdownify


@pytest.mark.parametrize(
    "text,converted",
    [
        ("# foo", "<h1># foo</h1>"),
        ("## foo", "<h2>## foo</h2>"),
        ("### foo", "<h3>### foo</h3>"),
        ("#### foo", "<h4>#### foo</h4>"),
    ],
)
def test_headers(text, converted):
    assert markdownify([text])[0] == converted


@pytest.mark.parametrize(
    "text,converted",
    [
        ("**foo**", "<b>**foo**</b><br/>"),
        ("**foo**.", "<b>**foo**</b>.<br/>"),
        ("**foo**. aaa", "<b>**foo**</b>. aaa<br/>"),
        (" **foo**", " <b>**foo**</b><br/>"),
        (" **foo** ", " <b>**foo**</b> <br/>"),
        ("**foo** ", "<b>**foo**</b> <br/>"),
        ("**foo**: ", "<b>**foo**</b>: <br/>"),
        (" **foo bar** ", " <b>**foo bar**</b> <br/>"),
    ],
)
def test_bold(text, converted):
    assert markdownify([text])[0] == converted


@pytest.mark.parametrize(
    "text,converted",
    [
        ("**foo**", "<b>foo</b><br/>"),
        ("**foo**.", "<b>foo</b>.<br/>"),
        ("**foo**. aaa", "<b>foo</b>. aaa<br/>"),
        (" **foo**", " <b>foo</b><br/>"),
        (" **foo** ", " <b>foo</b> <br/>"),
        ("**foo** ", "<b>foo</b> <br/>"),
        ("**foo**: ", "<b>foo</b>: <br/>"),
        (" **foo bar** ", " <b>foo bar</b> <br/>"),
    ],
)
def test_bold_hidden(text, converted):
    assert markdownify([text], visible=False)[0] == converted


@pytest.mark.parametrize(
    "text,converted",
    [
        ("_foo_", "<i>_foo_</i><br/>"),
        ("_foo_.", "<i>_foo_</i>.<br/>"),
        ("_foo_. aaa", "<i>_foo_</i>. aaa<br/>"),
        (" _foo_", " <i>_foo_</i><br/>"),
        (" _foo_ ", " <i>_foo_</i> <br/>"),
        ("_foo_ ", "<i>_foo_</i> <br/>"),
        ("_foo_: ", "<i>_foo_</i>: <br/>"),
        (" _foo bar_ ", " <i>_foo bar_</i> <br/>"),
    ],
)
def test_italics(text, converted):
    assert markdownify([text])[0] == converted


@pytest.mark.parametrize(
    "text,converted",
    [
        ("_foo_", "<i>foo</i><br/>"),
        ("_foo_.", "<i>foo</i>.<br/>"),
        ("_foo_. aaa", "<i>foo</i>. aaa<br/>"),
        (" _foo_", " <i>foo</i><br/>"),
        (" _foo_ ", " <i>foo</i> <br/>"),
        ("_foo_ ", "<i>foo</i> <br/>"),
        ("_foo_: ", "<i>foo</i>: <br/>"),
        (" _foo bar_ ", " <i>foo bar</i> <br/>"),
    ],
)
def test_italics_hidden(text, converted):
    assert markdownify([text], visible=False)[0] == converted


@pytest.mark.parametrize(
    "text,converted",
    [
        ("`foo`", "<tt>`foo`</tt><br/>"),
        ("`foo`.", "<tt>`foo`</tt>.<br/>"),
        ("`foo`. aaa", "<tt>`foo`</tt>. aaa<br/>"),
        (" `foo`", " <tt>`foo`</tt><br/>"),
        (" `foo` ", " <tt>`foo`</tt> <br/>"),
        ("`foo` ", "<tt>`foo`</tt> <br/>"),
        ("`foo`: ", "<tt>`foo`</tt>: <br/>"),
        (" `foo bar` ", " <tt>`foo bar`</tt> <br/>"),
    ],
)
def test_tt(text, converted):
    assert markdownify([text])[0] == converted


@pytest.mark.parametrize(
    "text,converted",
    [
        ("`foo`", "<tt>foo</tt><br/>"),
        ("`foo`.", "<tt>foo</tt>.<br/>"),
        ("`foo`. aaa", "<tt>foo</tt>. aaa<br/>"),
        (" `foo`", " <tt>foo</tt><br/>"),
        (" `foo` ", " <tt>foo</tt> <br/>"),
        ("`foo` ", "<tt>foo</tt> <br/>"),
        ("`foo`: ", "<tt>foo</tt>: <br/>"),
        (" `foo bar` ", " <tt>foo bar</tt> <br/>"),
    ],
)
def test_tt_hidden(text, converted):
    assert markdownify([text], visible=False)[0] == converted


@pytest.mark.parametrize(
    "text,converted",
    [
        ("_f_oo_", "<i>_f_oo_</i><br/>"),
        (" _f_oo_", " <i>_f_oo_</i><br/>"),
        (" _f_oo_ ", " <i>_f_oo_</i> <br/>"),
        ("_f_oo_ ", "<i>_f_oo_</i> <br/>"),
        (" _f_oo bar_ ", " <i>_f_oo bar_</i> <br/>"),
    ],
)
def test_escaped_underscore(text, converted):
    assert markdownify([text])[0] == converted

test/test_markdownify.py
rberenguel/PiWrite