Beispiel #1
0
def imfeelinglucky(uid):
    """
    Try to make most straightforward action on note,
    similar in spirit to "I'm feeling lucky" feature in Google search.
    :param uid:
    :return:
    """
    subprocess.Popen(
        "increase_hit_history_for pyjoplin %s" % inspect.currentframe().f_code.co_name,
        shell=True,
    )

    # NOTE: For now only implements searching first code stub in Solution:
    # In other cases it could open url for "link-type" notes

    # Find note entry by uid
    note = Note.get(Note.id == uid)

    # Parse first code stub (if any)
    import re

    stub = ""
    if not stub:
        # Typical example:
        # # Solution...
        # ... (one or several lines)
        # ```<lang>
        # <code_stub>
        # ```
        pattern_str = r"#?\s*Solution.*?```.*?\n(.*?)```"
        m = re.search(pattern_str, note.body, re.DOTALL)
        if m:
            stub = m.group(1)
    if not stub:
        # Inline solution:
        # # Solution...
        # ... (one or several lines)
        # `<code_stub>`
        pattern_str = r"#?\s*Solution.*?`(.*?)`"
        m = re.search(pattern_str, note.body, re.DOTALL)
        if m:
            stub = m.group(1)
    if not stub:
        notification.show("No code stub found, opening file", note.title)
        edit(uid)
        return None
    # Strip newlines to avoid unexpected execution e.g. in shell
    stub = stub.strip("\n")

    # Copy stub into the clipboard
    # NOTE: Using xclip because pyperclip does not work
    # Create temporary file with text
    path_tempfile = os.path.join(config.PATH_TEMP, "stub")
    with open(path_tempfile, "w") as f:
        f.write(stub)
    cmd = "xclip -selection clipboard %s" % path_tempfile
    subprocess.call(cmd, shell=True)

    notification.show("Extracted code stub:", note.title, stub)
    return stub
Beispiel #2
0
def rebuild_fts_index():
    """
    Rebuild virtual table for FTS (fulltext search)
    populating with all existing notes

    :return:
    """
    # Create empty FTS table from scratch
    try:
        # Remove table in case it existed
        NoteIndex.drop_table()
    except Exception:
        pass
    NoteIndex.create_table()

    # Add all entries into virtual FTS table
    notes = Note.select()
    with db.atomic():
        for note in notes:
            NoteIndex.insert(
                {
                    NoteIndex.uid: note.id,
                    NoteIndex.title: note.title,
                    NoteIndex.body: note.body,
                }
            ).execute()

    if config.DO_NOTIFY:
        notification.show("Rebuilt index", message="FTS index populated from scratch")
Beispiel #3
0
def new(title, notebook_name="personal", body=""):
    """
    Create new note in notebook
    :param title:
    :param notebook_name: optional
    :param body: optional
    :return: new_note.id
    """

    # Ensure that note with the same title does not exist already in the database
    try:
        note = Note.get(Note.title == title)
        notification.show_error("Note with same name already exists", note.title)
        raise Exception(f"Note with same name already exists")
    except Note.DoesNotExist:
        # All good, it should not exist
        pass

    # Retrieve notebook id
    try:
        notebook = Folder.get(Folder.title == notebook_name)
    except Folder.DoesNotExist:
        notification.show_error("Notebook not found", notebook_name)
        raise Folder.DoesNotExist

    # Create new note instance
    # Get unique identifier following Joplin format
    # Source: https://github.com/laurent22/joplin/issues/305#issuecomment-374123414
    import uuid

    uid = str(uuid.uuid4()).replace("-", "")
    # Get current time in Joplin format (int epoch in milliseconds)
    uint_current_timestamp_msec = time_joplin()
    new_note = Note(
        body=body,
        created_time=uint_current_timestamp_msec,
        id=uid,
        parent=notebook.id,
        source="pyjoplin",
        source_application="pyjoplin",
        title=title,
        updated_time=uint_current_timestamp_msec,
        user_created_time=uint_current_timestamp_msec,
        user_updated_time=uint_current_timestamp_msec,
    )
    num_saved_notes = new_note.save(force_insert=True)
    if num_saved_notes != 1:
        notification.show_error_and_raise("Creating note", new_note.title)
    if config.DO_NOTIFY:
        notification.show("New note created in nb %s" % notebook.title, title)

    return new_note.id
Beispiel #4
0
def setup():
    """
    Do setup required by pyjoplin for interfacing with desktop and terminal app
    NOTE: db is shared by terminal and desktop apps
    :return:
    """
    # Substitute CLI database by symbolic link to desktop database.sqlite
    subprocess.call(
        "rm database.sqlite", shell=True, cwd=os.path.expanduser("~/.config/joplin/")
    )
    subprocess.call(
        "ln -s ../joplin-desktop/database.sqlite",
        shell=True,
        cwd=os.path.expanduser("~/.config/joplin/"),
    )

    if config.DO_NOTIFY:
        notification.show("Setup succeeded")
Beispiel #5
0
    def from_file(self, file_path):
        """
        Populate this note title, notebook and body from a text file
        NOTE: This does not save the note in the database, just changes its content
        :param file_path:
        :return:
        """
        # Save new content to Notes table
        # NOTE: FTS entry is automatically updated within .save()

        # Check empty case first, for removal
        if os.path.getsize(file_path) <= 1:
            # The file is empty, should trigger removal
            # NOTE: I don't remember now how removal worked, so trying the below
            self.title = ""
            self.body = ""
            return

        # NOTE:
        #   There is no `from_string` method because I did not need that yet
        #   Besides, processing from a string vs a file may have a couple of nits
        #   See https://stackoverflow.com/questions/7472839/python-readline-from-a-string
        with open(file_path, "r", encoding="utf-8") as f:
            # Get UID from first line
            uid_line = f.readline().strip()
            assert uid_line == self.id

            # Get summary from second line
            self.title = f.readline().strip()

            # Get notebook name from third line
            notebook_name_line = f.readline().strip()
            try:
                assert notebook_name_line.startswith("#")
            except AssertionError:
                notification.show_error(
                    "Bad notebook line format",
                    message="Lines is: %s\nShould start with #" %
                    notebook_name_line,
                )
                raise RuntimeError("Bad notebook line format")
            notebook_name = notebook_name_line[1:]

            # React to notebook changes from text editor
            try:
                notebook = Folder.get(Folder.title == notebook_name)
                if self.parent is not None and notebook.id != self.parent:
                    previous_notebook = Folder.get(Folder.id == self.parent)
                    notification.show(
                        "Notebook changed",
                        note_title=self.title,
                        message="Changed from #%s to #%s" %
                        (previous_notebook.title, notebook.title),
                    )
                self.parent = notebook.id
            except Folder.DoesNotExist:
                previous_notebook = Folder.get(Folder.id == self.parent)
                notification.show_error(
                    "Notebook not found",
                    message="#%s\nSaving to previous notebook #%s instead" %
                    (notebook_name, previous_notebook.title),
                )

            # Strip dates line
            f.readline()  # mdate
            f.readline()  # cdate

            # Assert white line afterwards
            assert not f.readline().strip()

            # Read rest of file as body
            self.body = f.read().strip()
Beispiel #6
0
def delete(uid):
    # Find note entry by uid
    note = Note.get(Note.id == uid)
    note.delete_instance()
    if config.DO_NOTIFY:
        notification.show("Deleted note", note.title)
Beispiel #7
0
def edit(uid):
    subprocess.Popen(
        "increase_hit_history_for pyjoplin %s" % inspect.currentframe().f_code.co_name,
        shell=True,
    )

    # Find note entry by uid
    note = Note.get(Note.id == uid)
    # Save previous title and body for reference
    init_title = note.title
    init_body = note.body

    # Populate temporary file from note content
    path_sentinel = os.path.join(config.PATH_TEMP, f"{uid}")
    if os.path.exists(path_sentinel):
        notification.show_error("Note is already under edit", note.title, note.id)
        raise Exception("Note is already under edit")
    else:
        # Create sentinel to block this note
        open(path_sentinel, "a").close()

    path_tempfile = os.path.join(config.PATH_TEMP, f"{note.title.replace('/', '_')}.md")

    # Create named symlink to sentinel note
    # for better readability
    # NOTE:
    #   Because of this link, the sentinel is no longer *just* a sentinel.
    #   Instead, it is a plain text file containing the actual note content.
    try:
        os.symlink(path_sentinel, path_tempfile)
    except FileExistsError:
        notification.show_error(
            "Note file with same name is already under edit", note.title, note.id
        )
        raise Exception(f"Temp file named {note.title} already exists")

    note.to_file(path_tempfile)

    # Open file with editor
    # NOTE: Stop using template, this command gets too complicated for a single string
    #       It's better to have a list of inputs to build the last complicated command
    #       Nesting bash becomes necessary to execute source for non-interactive customization
    # NOTE: Using vimx since aliases do not seem to apply in non-interactive (even if I define it inside bashrc!?)
    proc = subprocess.Popen(
        # [
        #     'xfce4-terminal',
        #     '--disable-server',
        #     '--title',
        #     'pyjoplin - {title}'.format(title=note.title),
        #     '-e',
        #     'bash -c "source ~/.bashrc && unset PYTHONPATH && vimx \'{path}\' || vi \'{path}\'"'.format(path=path_tempfile)
        # ],
        [
            "run-standalone.sh",
            "bash",
            "-c",
            # NOTE: `stty -ixon` to disable "flow control characters" for vim-shell
            # NOTE: `unset PYTHONPATH` seems necessary for this to work when called through ulauncher extension
            f"stty -ixon && unset PYTHONPATH && vim -u ~/Code/python/pyjoplin/vim/vimrc '{path_tempfile}'"
            # NOTE: Version below works when called from terminal,
            # e.g. `pyjoplin edit 170b3c8de5034f7c8023a6a39f02219c`
            # but it immediately exits when called via ulauncher
            # 'vimx',
            # '{path}'.format(path=path_tempfile)
        ],
        env=dict(os.environ, OPT_TITLE=f"pyjoplin - {note.title}"),
        stdout=subprocess.PIPE,
    )
    # cmd_str = config.EDITOR_CALL_TEMPLATE.format(title=note.title, path=path_tempfile)
    # proc = subprocess.Popen(
    #     cmd_str, shell=True,
    #     # NOTE: This is needed for non-gedit editors, but DISPLAY seems to be giving issues
    #     # env=dict(
    #     #     os.environ,
    #     #     PYTHONPATH='',  # avoid issues when calling Python3 scripts from Python2
    #     #     DISPLAY=":0.0"  # ensure some GUI apps catch the right display
    #     # ),
    #     stdout=subprocess.PIPE
    # )

    # Loop during edition to save incremental changes
    import time

    last_modification_time_sec = os.path.getmtime(path_tempfile)
    try:
        # Sync note under edition with database note in a loop
        while proc.poll() is None:
            time.sleep(0.5)
            if os.path.getmtime(path_tempfile) > last_modification_time_sec:
                last_modification_time_sec = os.path.getmtime(path_tempfile)

                note.from_file(path_tempfile)
                num_saved_notes = note.save()
                if num_saved_notes != 1:
                    notification.show_error(
                        "Saving note changes during edition", note.title
                    )
                # else:
                #     notification.show("Note saved", note.title)
    except:
        notification.show_error("During note edition sync loop", note.title)
        raise Exception("ERROR during editor-database sync loop")

    returncode = proc.wait()
    if returncode != 0:
        print("ERROR during edition")
        print("Output:")
        output = proc.stdout.read().decode("utf-8")
        print(output)
        notification.show_error(
            "returncode from editor was not 0. Inspect note to sanity-check.",
            note.title,
            output,
        )
        raise Exception("ERROR during edition")

    # Save edited file content to Notes table
    note.from_file(path_tempfile)

    # Cleanup temp files
    # NOTE:
    # If the editor finished with some error code
    # and things didn't go well,
    # we do NOT remove the sentinel.
    # So we will get an error when trying to open the note again, showcasing the issue
    # (even though you should have got an error notification, but just in case!)
    os.remove(path_tempfile)
    os.remove(path_sentinel)

    # Check for note changes
    if note.title == init_title and note.body == init_body:
        # Changed nothing, no need to save
        if config.DO_NOTIFY:
            notification.show("Finished edition with no changes", note.title)
        return

    # Delete entry if empty
    if (not note.title) and (not note.body):
        note.delete_instance()
        if config.DO_NOTIFY:
            notification.show("Deleted note", init_title)
        return

    # Save note changes into database
    # NOTE: FTS entry is automatically updated within .save()
    num_saved_notes = note.save()
    if num_saved_notes != 1:
        notification.show_error_and_raise("Saving note changes", note.title)

    if config.DO_NOTIFY:
        notification.show("Note saved", note.title)