Exemple #1
0
def rename_conflicting_notes():
    notes = Note.select().where(Note.is_conflict == 1)
    with db.atomic():
        for note in notes:
            if not note.title.endswith("(CONFLICT)"):
                note.title = note.title + " (CONFLICT)"
                num_saved_notes = note.save()
                if num_saved_notes != 1:
                    notification.show_error("Renaming conflicting note", note.title)
Exemple #2
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
Exemple #3
0
 def to_string(self):
     try:
         notebook = Folder.get(Folder.id == self.parent)
     except Folder.DoesNotExist:
         notification.show_error("Notebook not found",
                                 message="nb id %s" % self.parent)
         raise Folder.DoesNotExist
     dates_line = (
         f"mdate={datetime.utcfromtimestamp(self.updated_time/1000).strftime('%Y-%m-%d')}\n"
         f"cdate={datetime.utcfromtimestamp(self.created_time/1000).strftime('%Y-%m-%d')}"
     )
     return (
         f"{self.id}\n{self.title}\n#{notebook.title}\n{dates_line}\n\n{self.body}"
     )
Exemple #4
0
 def delete_instance(self, *args, **kwargs):
     NoteIndex.remove_note(self)
     try:
         # Register item deletion to be synced
         deletion_item = DeletedItems.create(deleted_time=time_joplin(),
                                             item=self.id,
                                             item_type=1,
                                             sync_target=7)
     except:
         notification.show_error(
             "DeletedItems table",
             message="Creating deletion item for Dropbox sync\nNote: %s" %
             (self.title),
         )
     return super(Note, self).delete_instance(*args, **kwargs)
Exemple #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()
Exemple #6
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)
printc = lambda x: print(colored(x, 'cyan'))

notes = Note.select().order_by(Note.title)
print("Listing empty notes:")
for note in notes:
    if not note.body:
        printc('Empty: %s %s' % (note.id, note.title))
        notefile = '%s.md' % note.id
        if path_exportdir / notefile in path_exportdir.iterdir():
            note.from_file(str(path_exportdir / notefile))
            filtered_lines = list()
            for line in note.body.split('\n'):
                if line.startswith('id: '):
                    break
                filtered_lines.append(line)
            note.body = '\n'.join(filtered_lines)
            if not note.body:
                continue
            printc('Found non-empty export file:')
            print(note.title)
            print('')
            print(note.body)
            yn = raw_input('Save loaded note content? (Y/n)')
            if yn == 'y' or yn == '':
                num_saved_notes = note.save()
                if num_saved_notes != 1:
                    notification.show_error("Saving note changes during edition", note.title)