Example #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
Example #2
0
def edit_by_title(title):
    if isinstance(title, list):
        # For case coming from CLI
        title = " ".join(title)
    try:
        note = Note.get(Note.title == title.rstrip())
        edit(note.id)
    except Note.DoesNotExist:
        print("No exact match found for title query " + title)
Example #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
Example #4
0
def print_for_uid(uid):
    """
    Print note content
    :param uid:
    :return: returncode
    """
    try:
        note = Note.get(Note.id == uid)
        print(note.to_string(), end="")
        return 0
    except Note.DoesNotExist:
        print(f"No note found with id {uid}")
        return 2
Example #5
0
    def test_note_to_file_with_unicode(self):
        test_title = 'pyjoplin_test test_note_to_file_with_unicode'
        test_body = u"This is a unicode string: \u2192"

        note_id = commands.new(test_title, 'test', body=test_body)
        new_note = Note.get(Note.id == note_id)

        path_tempfile = '/tmp/pyjoplin/test_note_to_file_with_unicode'
        new_note.to_file(path_tempfile)

        backup_body = new_note.body
        new_note.from_file(path_tempfile)

        self.assertEqual(new_note.body, backup_body)
Example #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)
Example #7
0
def find_title(title):
    try:
        note = Note.get(Note.title == title)
        return note.id
    except Note.DoesNotExist:
        return None
Example #8
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)