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)
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
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}" )
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)
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()
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)