├── src ├── __init__.py ├── moment.py ├── items.py └── functions.py ├── test-vault ├── Test2.md ├── Test.md ├── subdir │ ├── Test.md │ └── Hallo.md └── .obsidian │ ├── community-plugins.json │ └── plugins │ └── periodic-notes │ └── data.json ├── run_dev.sh ├── images ├── icon.png ├── icon-add.png └── icon.svg ├── screenshot.png ├── versions.json ├── run_dev_extension.sh ├── README.md ├── LICENSE ├── manifest.json ├── .gitignore └── main.py /src/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test-vault/Test2.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-vault/Test.md: -------------------------------------------------------------------------------- 1 | This is a test -------------------------------------------------------------------------------- /test-vault/subdir/Test.md: -------------------------------------------------------------------------------- 1 | Another Test -------------------------------------------------------------------------------- /test-vault/subdir/Hallo.md: -------------------------------------------------------------------------------- 1 | I just want to say Hallo -------------------------------------------------------------------------------- /run_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ulauncher --no-extensions --dev -v -------------------------------------------------------------------------------- /test-vault/.obsidian/community-plugins.json: -------------------------------------------------------------------------------- 1 | ["periodic-notes"] 2 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebarkmin/ulauncher-obsidian/HEAD/images/icon.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebarkmin/ulauncher-obsidian/HEAD/screenshot.png -------------------------------------------------------------------------------- /images/icon-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebarkmin/ulauncher-obsidian/HEAD/images/icon-add.png -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "required_api_version": "^2.0.0", 4 | "commit": "main" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /run_dev_extension.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ln -sfn $PWD ~/.local/share/ulauncher/extensions/ulauncher-obsidian 3 | VERBOSE=1 ULAUNCHER_WS_API=ws://127.0.0.1:5054/ulauncher-obsidian PYTHONPATH=/usr/lib/python3/dist-packages /usr/bin/python3 $PWD/main.py 4 | -------------------------------------------------------------------------------- /test-vault/.obsidian/plugins/periodic-notes/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "showGettingStartedBanner": true, 3 | "hasMigratedDailyNoteSettings": false, 4 | "hasMigratedWeeklyNoteSettings": false, 5 | "daily": { 6 | "format": "DD-MM-YYYY", 7 | "template": "", 8 | "folder": "", 9 | "enabled": true 10 | }, 11 | "weekly": { 12 | "format": "", 13 | "template": "", 14 | "folder": "" 15 | }, 16 | "monthly": { 17 | "format": "", 18 | "template": "", 19 | "folder": "" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/moment.py: -------------------------------------------------------------------------------- 1 | # the order is important 2 | token_map = { 3 | "%": "%%", 4 | "A": "%p", 5 | "ww": "%W", 6 | "dddd": "%A", 7 | "ddd": "%a", 8 | "d": "%w", 9 | "MMMM": "%B", 10 | "MMM": "%b", 11 | "MM": "%m", 12 | "YYYY": "%Y", 13 | "YY": "%y", 14 | "HH": "%H", 15 | "hh": "%I", 16 | "mm": "%M", 17 | "SSS": "%f", 18 | "ss": "%S", 19 | "ZZ": "%z", 20 | "z": "%Z", 21 | "DDDD": "%j", 22 | "DD": "%d", 23 | } 24 | 25 | def convert_moment_to_strptime_format(moment_date: str): 26 | """ 27 | >>> convert_moment_to_strptime_format("YYYY-MM-DD") 28 | '%Y-%m-%d' 29 | """ 30 | strptime_date = moment_date 31 | for [mt, st] in token_map.items(): 32 | strptime_date = strptime_date.replace(mt, st) 33 | return strptime_date 34 | 35 | 36 | if __name__ == "__main__": 37 | import doctest 38 | 39 | doctest.testmod() 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Ulauncher extension 2 | 3 | ![Screenshot](screenshot.png) 4 | 5 | This Ulauncher extension enables you to search your [obsidian.md](https://obsidian.md/) vault and create new notes on the fly. 6 | 7 | ## Features 8 | 9 | Keywords are customizable 10 | 11 | * on: Open note based on filename 12 | * of: Search the content of all notes 13 | * od: Open daily note 14 | * oc: Quick capture to a note 15 | 16 | ## Install 17 | 18 | Then open Ulauncher preferences window > extensions > add extension and paste the following url: 19 | 20 | ``` 21 | https://github.com/mikebarkmin/ulauncher-obsidian 22 | ``` 23 | 24 | ## Developer 25 | 26 | ### 27 | 28 | ### Run Test 29 | 30 | Currently, doctest is used for the `functions` and `moment` module. To run the tests execute the following command: 31 | 32 | Install time_machine: 33 | ``` 34 | pip install time_machine 35 | ``` 36 | 37 | ``` 38 | python3 -m src.functions 39 | python3 -m src.moment 40 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mike Barkmin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Obsidian", 3 | "required_api_version": "^2.0.0", 4 | "description": "Manage your Obsidian Vault", 5 | "developer_name": "Mike Barkmin", 6 | "icon": "images/icon.png", 7 | "options": { 8 | "query_debounce": 0.1 9 | }, 10 | "preferences": [ 11 | { 12 | "id": "obsidian_vault", 13 | "type": "input", 14 | "name": "Vault", 15 | "description": "Path to your vault (absolute)", 16 | "default_value": "" 17 | }, 18 | { 19 | "id": "obsidian_search_note_vault", 20 | "type": "keyword", 21 | "name": "Search Note", 22 | "description": "Search the name your notes", 23 | "default_value": "on" 24 | }, 25 | { 26 | "id": "obsidian_search_string_vault", 27 | "type": "keyword", 28 | "name": "Search String", 29 | "description": "Search the content of your notes", 30 | "default_value": "of" 31 | }, 32 | { 33 | "id": "obsidian_open_daily", 34 | "type": "keyword", 35 | "name": "Open Daily", 36 | "description": "Open the daily note", 37 | "default_value": "od" 38 | }, 39 | { 40 | "id": "obsidian_quick_capture", 41 | "type": "keyword", 42 | "name": "Quick Capture", 43 | "description": "Append text to your quick capture note", 44 | "default_value": "oc" 45 | }, 46 | { 47 | "id": "obsidian_quick_capture_note", 48 | "type": "input", 49 | "name": "Quick Capture Note", 50 | "description": "The note used for quick capture. If empty the daily note will be used.", 51 | "default_value": "" 52 | }, 53 | { 54 | "id": "number_of_notes", 55 | "type": "input", 56 | "name": "Limit the number notes to select", 57 | "default_value": 8 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /src/items.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from ulauncher.api.shared.item.ExtensionResultItem import ExtensionResultItem 3 | from ulauncher.api.shared.action.OpenAction import OpenAction 4 | from ulauncher.api.shared.action.ExtensionCustomAction import ExtensionCustomAction 5 | 6 | from .functions import generate_url, Note 7 | 8 | 9 | ICON_FILE = "images/icon.png" 10 | ICON_ADD_FILE = "images/icon-add.png" 11 | 12 | 13 | def create_note(name): 14 | return [ 15 | ExtensionResultItem( 16 | icon=ICON_ADD_FILE, 17 | name=f'Create "{name}"', 18 | on_enter=ExtensionCustomAction( 19 | {"type": "create-note", "name": name}, keep_app_open=True 20 | ), 21 | ) 22 | ] 23 | 24 | 25 | def quick_capture_note(content): 26 | return [ 27 | ExtensionResultItem( 28 | icon=ICON_ADD_FILE, 29 | name="Quick Capture", 30 | on_enter=ExtensionCustomAction( 31 | {"type": "quick-capture", "content": content}, keep_app_open=True 32 | ), 33 | ), 34 | ExtensionResultItem( 35 | icon=ICON_ADD_FILE, 36 | name="Quick Capture to Note", 37 | on_enter=ExtensionCustomAction( 38 | {"type": "quick-capture-to-note", "content": content}, 39 | keep_app_open=True, 40 | ), 41 | ), 42 | ] 43 | 44 | 45 | def show_notes(vault, notes: List[Note], limit = 10): 46 | return [ 47 | ExtensionResultItem( 48 | icon=ICON_FILE, 49 | name=note.name, 50 | description=note.description, 51 | on_enter=OpenAction(generate_url(vault, note.path)), 52 | ) 53 | for note in notes[:limit] 54 | ] 55 | 56 | 57 | def select_note(notes: List[Note], limit = 10): 58 | return [ 59 | ExtensionResultItem( 60 | icon=ICON_FILE, 61 | name=note.name, 62 | description=note.description, 63 | on_enter=ExtensionCustomAction( 64 | {"type": "select-note", "note": note}, 65 | keep_app_open=True, 66 | ), 67 | ) 68 | for note in notes[:limit] 69 | ] 70 | 71 | 72 | def cancel(): 73 | return [ 74 | ExtensionResultItem( 75 | icon=ICON_FILE, 76 | name="Cancel", 77 | on_enter=ExtensionCustomAction({"type": "cancel"}, keep_app_open=True), 78 | ) 79 | ] 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import gi 2 | from ulauncher.api.shared.action.ExtensionCustomAction import ExtensionCustomAction 3 | 4 | gi.require_version("Gdk", "3.0") 5 | from src.items import quick_capture_note, show_notes, create_note, select_note, cancel 6 | from src.functions import ( 7 | append_to_note_in_vault, 8 | find_note_in_vault, 9 | find_string_in_vault, 10 | create_note_in_vault, 11 | generate_daily_url, 12 | generate_url, 13 | ) 14 | from ulauncher.api.client.Extension import Extension 15 | from ulauncher.api.client.EventListener import EventListener 16 | from ulauncher.api.shared.event import ( 17 | KeywordQueryEvent, 18 | ItemEnterEvent, 19 | SystemExitEvent, 20 | ) 21 | from ulauncher.api.shared.action.RenderResultListAction import RenderResultListAction 22 | from ulauncher.api.shared.action.OpenAction import OpenAction 23 | from ulauncher.api.shared.action.DoNothingAction import DoNothingAction 24 | from ulauncher.api.shared.action.HideWindowAction import HideWindowAction 25 | from ulauncher.api.shared.action.SetUserQueryAction import SetUserQueryAction 26 | import logging 27 | 28 | logger = logging.getLogger(__name__) 29 | 30 | 31 | class ObisidanExtension(Extension): 32 | def __init__(self): 33 | super(ObisidanExtension, self).__init__() 34 | 35 | self.state = "default" 36 | self.content = "" 37 | self.subscribe(KeywordQueryEvent, KeywordQueryEventListener()) 38 | self.subscribe(ItemEnterEvent, ItemEnterEventListener()) 39 | self.subscribe(SystemExitEvent, SystemExitEventListener()) 40 | 41 | def reset(self): 42 | self.state = "default" 43 | self.content = "" 44 | 45 | 46 | class ItemEnterEventListener(EventListener): 47 | def on_event(self, event, extension): 48 | vault = extension.preferences["obsidian_vault"] 49 | data = event.get_data() 50 | type = data.get("type") 51 | 52 | if type == "cancel": 53 | extension.reset() 54 | return SetUserQueryAction("") 55 | 56 | elif type == "create-note" and extension.state == "quick-capture-to-note": 57 | path = create_note_in_vault(vault, data.get("name")) 58 | append_to_note_in_vault(vault, path, extension.content) 59 | extension.reset() 60 | return HideWindowAction() 61 | 62 | elif type == "create-note": 63 | path = create_note_in_vault(vault, data.get("name")) 64 | url = generate_url(vault, path) 65 | return OpenAction(url) 66 | 67 | elif type == "quick-capture": 68 | quick_capute_note = extension.preferences["obsidian_quick_capture_note"] 69 | append_to_note_in_vault(vault, quick_capute_note, data.get("content")) 70 | return HideWindowAction() 71 | 72 | elif type == "quick-capture-to-note": 73 | keyword_quick_capture = extension.preferences["obsidian_quick_capture"] 74 | extension.state = "quick-capture-to-note" 75 | extension.content = data.get("content") 76 | return SetUserQueryAction(keyword_quick_capture + " ") 77 | 78 | elif extension.state == "quick-capture-to-note" and type == "select-note": 79 | quick_capute_note = data.get("note").path 80 | append_to_note_in_vault(vault, quick_capute_note, extension.content) 81 | extension.reset() 82 | return HideWindowAction() 83 | 84 | return DoNothingAction() 85 | 86 | 87 | class KeywordQueryEventListener(EventListener): 88 | def on_event(self, event, extension): 89 | vault = extension.preferences["obsidian_vault"] 90 | 91 | keyword_search_note_vault = extension.preferences["obsidian_search_note_vault"] 92 | keyword_search_string_vault = extension.preferences[ 93 | "obsidian_search_string_vault" 94 | ] 95 | keyword_open_daily = extension.preferences["obsidian_open_daily"] 96 | keyword_quick_capture = extension.preferences["obsidian_quick_capture"] 97 | number_of_notes = int(extension.preferences.get("number_of_notes", 8)) 98 | 99 | keyword = event.get_keyword() 100 | search = event.get_argument() 101 | 102 | if extension.state == "quick-capture-to-note": 103 | notes = find_note_in_vault(vault, search) 104 | items = select_note(notes, number_of_notes) 105 | items += create_note(search) 106 | items += cancel() 107 | return RenderResultListAction(items) 108 | 109 | if keyword == keyword_search_note_vault: 110 | notes = find_note_in_vault(vault, search) 111 | items = show_notes(vault, notes, number_of_notes) 112 | items += create_note(search) 113 | items += cancel() 114 | return RenderResultListAction(items) 115 | 116 | elif keyword == keyword_search_string_vault: 117 | notes = find_string_in_vault(vault, search) 118 | items = show_notes(vault, notes, number_of_notes) 119 | items += create_note(search) 120 | items += cancel() 121 | return RenderResultListAction(items) 122 | 123 | elif keyword == keyword_open_daily: 124 | return OpenAction(generate_daily_url(vault)) 125 | 126 | elif keyword == keyword_quick_capture: 127 | items = quick_capture_note(search) 128 | return RenderResultListAction(items) 129 | 130 | return DoNothingAction() 131 | 132 | 133 | class SystemExitEventListener(EventListener): 134 | def on_event(self, event, extension): 135 | extension.reset() 136 | 137 | 138 | if __name__ == "__main__": 139 | ObisidanExtension().run() 140 | -------------------------------------------------------------------------------- /src/functions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import json 4 | import datetime 5 | from urllib.parse import quote, urlencode 6 | from pathlib import Path 7 | from typing import List, Literal 8 | import logging 9 | from ulauncher.utils.fuzzy_search import get_score 10 | 11 | from .moment import convert_moment_to_strptime_format 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def fuzzyfinder(search: str, items: List[str]) -> List[str]: 17 | """ 18 | >>> fuzzyfinder("hallo", ["hi", "hu", "hallo", "false"]) 19 | ['hallo', 'false', 'hi', 'hu'] 20 | """ 21 | scores = [] 22 | for i in items: 23 | score = get_score(search, get_name_from_path(i)) 24 | scores.append((score, i)) 25 | 26 | scores = sorted(scores, key=lambda score: score[0], reverse=True) 27 | 28 | return list(map(lambda score: score[1], scores)) 29 | 30 | 31 | class Note: 32 | def __init__(self, name: str, path: str, description: str): 33 | self.name = name 34 | self.path = path 35 | self.description = description 36 | 37 | def __repr__(self): 38 | return f"Note<{self.path}>" 39 | 40 | 41 | def generate_url(vault: str, file: str, mode: Literal["open", "new"] = "open") -> str: 42 | """ 43 | >>> generate_url("~/vault", "test.md") 44 | 'obsidian://open?vault=vault&file=test.md' 45 | 46 | >>> generate_url("~/vault", "test") 47 | 'obsidian://open?vault=vault&file=test.md' 48 | 49 | >>> generate_url("~/vault", "~/vault/test") 50 | 'obsidian://open?vault=vault&file=test.md' 51 | 52 | >>> generate_url("~/vault", "~/vault/test") 53 | 'obsidian://open?vault=vault&file=test.md' 54 | 55 | >>> generate_url("~/vault", "Java - Programming Language") 56 | 'obsidian://open?vault=vault&file=Java%20-%20Programming%20Language.md' 57 | 58 | >>> generate_url("/home/kira/Documents/main_notes/", "Ulauncher Test", mode="new") 59 | 'obsidian://new?vault=main_notes&file=Ulauncher%20Test.md' 60 | 61 | >>> generate_url("/home/[me]/Development/ObsidianVaults/", "Ulauncher Test", mode="new") 62 | 'obsidian://new?vault=ObsidianVaults&file=Ulauncher%20Test.md' 63 | 64 | """ 65 | if vault.endswith("/"): 66 | vault = vault[:-1] 67 | 68 | vault_name = get_name_from_path(vault, exclude_ext=False) 69 | if not file.endswith(".md"): 70 | file = file + ".md" 71 | 72 | try: 73 | relative_file = Path(file).relative_to(vault) 74 | return ( 75 | "obsidian://" 76 | + mode 77 | + "?" 78 | + urlencode({"vault": vault_name, "file": relative_file}, quote_via=quote) 79 | ) 80 | except ValueError: 81 | if not file.endswith(".md"): 82 | file = file + ".md" 83 | return ( 84 | "obsidian://" 85 | + mode 86 | + "?" 87 | + urlencode({"vault": vault_name, "file": file}, quote_via=quote) 88 | ) 89 | 90 | 91 | class DailyPath: 92 | path: str 93 | date: str 94 | folder: str 95 | exists: bool 96 | 97 | def __init__(self, path, date, folder, exists) -> None: 98 | self.path = path 99 | self.date = date 100 | self.folder = folder 101 | self.exists = exists 102 | 103 | 104 | class DailySettings: 105 | format: str 106 | folder: str 107 | 108 | def __init__(self, format, folder) -> None: 109 | self.folder = folder 110 | self.format = format 111 | 112 | 113 | def get_daily_settings(vault: str) -> DailySettings: 114 | daily_notes_path = os.path.join(vault, ".obsidian", "daily-notes.json") 115 | try: 116 | f = open(daily_notes_path, "r") 117 | daily_notes_config = json.load(f) 118 | f.close() 119 | except: 120 | daily_notes_config = {} 121 | format = daily_notes_config.get("format", "YYYY-MM-DD") 122 | folder = daily_notes_config.get("folder", "") 123 | 124 | if format == "": 125 | format = "YYYY-MM-DD" 126 | 127 | return DailySettings(format, folder) 128 | 129 | 130 | def get_periodic_settings(vault: str) -> DailySettings: 131 | periodic_path = os.path.join( 132 | vault, ".obsidian", "plugins", "periodic-notes", "data.json" 133 | ) 134 | try: 135 | f = open(periodic_path) 136 | config = json.load(f) 137 | f.close() 138 | except: 139 | config = {} 140 | 141 | daily_config = config.get("daily", {}) 142 | format = daily_config.get("format", "YYYY-MM-DD") 143 | folder = daily_config.get("folder", "") 144 | 145 | if format == "": 146 | format = "YYYY-MM-DD" 147 | 148 | return DailySettings(format, folder) 149 | 150 | 151 | def is_obsidian_plugin_enabled(vault: str, name: str) -> bool: 152 | core = os.path.join(vault, ".obsidian", "core-plugins.json") 153 | community = os.path.join(vault, ".obsidian", "community-plugins.json") 154 | plugins = [] 155 | try: 156 | with open(core) as f: 157 | core = json.load(f) 158 | plugins += core 159 | except: 160 | pass 161 | 162 | try: 163 | with open(community) as f: 164 | community = json.load(f) 165 | plugins += community 166 | except: 167 | pass 168 | 169 | return name in plugins 170 | 171 | 172 | def get_daily_path(vault: str) -> DailyPath: 173 | if is_obsidian_plugin_enabled(vault, "periodic-notes"): 174 | settings = get_periodic_settings(vault) 175 | else: 176 | settings = get_daily_settings(vault) 177 | 178 | date = datetime.datetime.now().strftime( 179 | convert_moment_to_strptime_format(settings.format) 180 | ) 181 | path = os.path.join(vault, settings.folder, date + ".md") 182 | exists = os.path.exists(path) 183 | 184 | return DailyPath(path, date, settings.folder, exists) 185 | 186 | 187 | def generate_daily_url(vault: str) -> str: 188 | """ 189 | >>> generate_daily_url("test-vault") 190 | 'obsidian://new?vault=test-vault&file=16-07-2021.md' 191 | """ 192 | daily_path = get_daily_path(vault) 193 | mode = "new" 194 | if daily_path.exists: 195 | mode = "open" 196 | 197 | return generate_url( 198 | vault, os.path.join(daily_path.folder, daily_path.date), mode=mode 199 | ) 200 | 201 | 202 | def get_name_from_path(path: str, exclude_ext=True) -> str: 203 | """ 204 | >>> get_name_from_path("~/home/test/bla/hallo.md") 205 | 'hallo' 206 | 207 | >>> get_name_from_path("~/home/Google Drive/Brain 1.0", False) 208 | 'Brain 1.0' 209 | """ 210 | base = os.path.basename(path) 211 | if exclude_ext: 212 | split = os.path.splitext(base) 213 | return split[0] 214 | return base 215 | 216 | 217 | def find_note_in_vault(vault: str, search: str) -> List[Note]: 218 | """ 219 | >>> find_note_in_vault("test-vault", "Test") 220 | [Note, Note, Note, Note] 221 | """ 222 | search_pattern = os.path.join(vault, "**", "*.md") 223 | logger.info(search_pattern) 224 | files = glob.glob(search_pattern, recursive=True) 225 | suggestions = fuzzyfinder(search, files) 226 | return [ 227 | Note(name=get_name_from_path(s), path=s, description=s) for s in suggestions 228 | ] 229 | 230 | 231 | def find_string_in_vault(vault: str, search: str) -> List[Note]: 232 | """ 233 | >>> find_string_in_vault("test-vault", "Test") 234 | [Note, Note] 235 | """ 236 | files = glob.glob(os.path.join(vault, "**", "*.md"), recursive=True) 237 | 238 | suggestions = [] 239 | 240 | CONTEXT_SIZE = 10 241 | 242 | search = search.lower() 243 | for file in files: 244 | if os.path.isfile(file) and search is not None: 245 | with open(file, "r") as f: 246 | for line in f: 247 | left, sep, right = line.lower().partition(search) 248 | if sep: 249 | context = left[CONTEXT_SIZE:] + sep + right[:CONTEXT_SIZE] 250 | suggestions.append( 251 | Note( 252 | name=get_name_from_path(file), 253 | path=file, 254 | description=context, 255 | ) 256 | ) 257 | break 258 | 259 | return suggestions 260 | 261 | 262 | def create_note_in_vault(vault: str, name: str) -> str: 263 | path = os.path.join(vault, name + ".md") 264 | if not os.path.isfile(path): 265 | with open(path, "w") as f: 266 | f.write(f"# {name}") 267 | return path 268 | 269 | 270 | def append_to_note_in_vault(vault: str, file: str, content: str): 271 | if file == "": 272 | file = get_daily_path(vault).path 273 | elif not file.endswith(".md"): 274 | file = file + ".md" 275 | path = os.path.join(vault, file) 276 | 277 | with open(path, "a") as f: 278 | f.write(os.linesep) 279 | f.write(content) 280 | 281 | 282 | if __name__ == "__main__": 283 | import doctest 284 | import time_machine 285 | 286 | traveller = time_machine.travel(datetime.datetime(2021, 7, 16)) 287 | traveller.start() 288 | 289 | doctest.testmod() 290 | 291 | traveller.stop() 292 | -------------------------------------------------------------------------------- /images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 224 | --------------------------------------------------------------------------------