├── .github ├── dependabot.yml └── workflows │ ├── pr-packager.yml │ ├── release.yml │ └── test-plugin.yml ├── .gitignore ├── README.md ├── icon.png ├── plugin.json ├── plugin ├── main.py └── obsidian.py ├── requirements.txt └── run.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | time: '01:00' 8 | open-pull-requests-limit: 2 9 | -------------------------------------------------------------------------------- /.github/workflows/pr-packager.yml: -------------------------------------------------------------------------------- 1 | name: "PR-Packager" 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | env: 6 | PYTHON_VER: 3.8 7 | jobs: 8 | deps: 9 | name: "Build" 10 | runs-on: windows-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Set up Python ${{ env.PYTHON_VER }} 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ env.PYTHON_VER }} 18 | - uses: actions/cache@v2 19 | with: 20 | path: ~\AppData\Local\pip\Cache 21 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 22 | restore-keys: | 23 | ${{ runner.os }}-pip- 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install wheel 28 | pip install -r ./requirements.txt -t ./lib 29 | - name: Upload 30 | uses: actions/upload-artifact@v2 31 | with: 32 | name: artifact 33 | path: | 34 | ./** 35 | !./.git/ 36 | !./README.md/ 37 | !./.github/ 38 | !./assets/ -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release-Builder" 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ main ] 6 | tags-ignore: 7 | - 'v*' 8 | paths-ignore: 9 | - .github/workflows/* 10 | - README.md 11 | - assets/* 12 | env: 13 | PYTHON_VER: 3.8 14 | jobs: 15 | deps: 16 | if: ${{ github.ref == 'refs/heads/main' }} 17 | name: "Build" 18 | runs-on: windows-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 0 24 | - name: Set up Python ${{ env.PYTHON_VER }} 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ env.PYTHON_VER }} 28 | - uses: actions/cache@v2 29 | if: startsWith(runner.os, 'Windows') 30 | with: 31 | path: ~\AppData\Local\pip\Cache 32 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 33 | restore-keys: | 34 | ${{ runner.os }}-pip- 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install wheel 39 | pip install -r ./requirements.txt -t ./lib 40 | - name: Get Plugin's version 41 | id: version 42 | uses: notiz-dev/github-action-json-property@release 43 | with: 44 | path: 'plugin.json' 45 | prop_path: 'Version' 46 | - name: Package files 47 | run: | 48 | git clone https://github.com/Garulf/flow_commands/ bin 49 | pip install -r ./bin/requirements.txt 50 | python ./bin/commands.py package -n "${{github.event.repository.name}}.zip" 51 | - name: Publish 52 | uses: softprops/action-gh-release@v1 53 | with: 54 | draft: false 55 | files: "./${{github.event.repository.name}}.zip" 56 | tag_name: "v${{steps.version.outputs.prop}}" 57 | generate_release_notes: true 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /.github/workflows/test-plugin.yml: -------------------------------------------------------------------------------- 1 | name: "Test Plugin" 2 | on: 3 | pull_request: 4 | workflow_dispatch: 5 | push: 6 | tags-ignore: 7 | - 'v*' 8 | paths-ignore: 9 | - .github/workflows/* 10 | - README.md 11 | - assets/* 12 | env: 13 | PYTHON_VER: 3.8 14 | BRANCH: 'main' 15 | jobs: 16 | test_run: 17 | name: "Test Run" 18 | runs-on: windows-latest 19 | strategy: 20 | matrix: 21 | flow_tags: ['latest'] 22 | python_ver: ['3.8'] 23 | steps: 24 | - name: Checkout Plugin Repo 25 | uses: actions/checkout@v2 26 | with: 27 | path: ${{github.event.repository.name}} 28 | - name: Get Plugin's version 29 | if: ${{ github.ref != 'refs/heads/main' }} 30 | id: version 31 | uses: notiz-dev/github-action-json-property@release 32 | with: 33 | path: './${{github.event.repository.name}}/plugin.json' 34 | prop_path: 'Version' 35 | - name: Assert Version updated 36 | if: ${{ github.ref != 'refs/heads/main' }} 37 | run: | 38 | $release_ver = (Invoke-WebRequest -Uri "https://raw.githubusercontent.com/${{github.repository}}/main/plugin.json" | ConvertFrom-Json).Version 39 | $release_ver = $release_ver -replace "v", "" 40 | $this_ver = "${{steps.version.outputs.prop}}" 41 | echo "This version:" $this_ver 42 | echo "Release version:" $release_ver 43 | if ([System.Version]$this_ver -gt [System.Version]$release_ver) { 44 | exit 0 45 | } else { 46 | exit 1 47 | } 48 | - name: Get latest Version tag 49 | run: | 50 | if ("${{matrix.flow_tags}}" -eq 'latest') { 51 | $url = "https://api.github.com/repos/Flow-Launcher/Flow.Launcher/releases/latest" 52 | } else { 53 | $url = "https://api.github.com/repos/Flow-Launcher/Flow.Launcher/releases/tags/${{matrix.flow_tags}}" 54 | } 55 | $release = Invoke-WebRequest -Uri $url | ConvertFrom-Json 56 | $tag_name = $release.tag_name 57 | foreach ($asset in $release.assets) 58 | { 59 | if($asset.name -like '*setup.exe') { 60 | $download_url = $asset.browser_download_url 61 | $file_name = $asset.name 62 | break 63 | } 64 | } 65 | echo "DOWNLOAD_URL=$download_url" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 66 | echo "FILE_NAME=$file_name" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 67 | echo "TAG_NAME=$tag_name" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 68 | - name: Flow Launcher Cache 69 | uses: actions/cache@v2 70 | id: flow_cache 71 | with: 72 | path: | 73 | ~\AppData\Roaming\FlowLauncher\* 74 | !~\AppData\Roaming\FlowLauncher\Plugins\* 75 | ~\AppData\Local\FlowLauncher\** 76 | key: ${{ runner.os }}-flow-${{ env.TAG_NAME }} 77 | - name: Download Flow Launcher 78 | id: download 79 | if: steps.flow_cache.outputs.cache-hit != 'true' 80 | run: | 81 | curl.exe -L -o ${{ env.FILE_NAME }} ${{ env.DOWNLOAD_URL }} 82 | - name: Install Flow Launcher 83 | if: steps.flow_cache.outputs.cache-hit != 'true' 84 | run: .\${{ env.FILE_NAME }} 85 | shell: cmd 86 | - name: Move Plugin to plugins directory 87 | run: | 88 | $repo_path = Join-Path -Path $pwd -ChildPath ${{github.event.repository.name}} 89 | $plugin_path = Join-Path -Path $env:APPDATA -ChildPath 'FlowLauncher' | Join-Path -ChildPath 'Plugins' | Join-Path -ChildPath ${{github.event.repository.name}} 90 | if (Test-Path $plugin_path) 91 | { 92 | echo "Removing cached directory" 93 | Remove-Item $plugin_path 94 | } 95 | New-Item -ItemType SymbolicLink -Path $plugin_path -Target $repo_path 96 | echo "PLUGIN_PATH=$plugin_path" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 97 | - name: Set up Python 98 | uses: actions/setup-python@v2 99 | with: 100 | python-version: ${{ matrix.python_ver }} 101 | - uses: actions/cache@v2 102 | with: 103 | path: ~\AppData\Local\pip\Cache 104 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 105 | restore-keys: | 106 | ${{ runner.os }}-pip-${{ matrix.python_ver }} 107 | - name: Install dependencies 108 | run: | 109 | cd ${{ env.PLUGIN_PATH }} 110 | python -m pip install --upgrade pip 111 | pip install wheel 112 | pip install -r ./requirements.txt -t ./lib 113 | - name: Get Plugin's Execute file 114 | id: exe 115 | uses: notiz-dev/github-action-json-property@release 116 | with: 117 | path: '${{ env.PLUGIN_PATH }}/plugin.json' 118 | prop_path: 'ExecuteFileName' 119 | - name: Test Run 120 | run: | 121 | cd ${{ env.PLUGIN_PATH }} 122 | python ${{steps.exe.outputs.prop}} 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | # Pyre type checker 114 | .pyre/ 115 | 116 | # Flox library folder 117 | flox 118 | 119 | # VScode 120 | 121 | .vscode/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian 2 | Search Obsidian notes 3 | 4 | ![image](https://user-images.githubusercontent.com/535299/148343248-f7795b10-969c-4f36-a6c1-bc34093bab5a.png) 5 | 6 | # 7 | Buy Me A Coffee 8 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Garulf/Obsidian-Notes/45e57f3862d933febbb17b530f622bfb1ee60e25/icon.png -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "F0DC45CD032F438C9B900679BD9B117A", 3 | "ActionKeyword": "ob", 4 | "Name": "Obsidian Notes", 5 | "Description": "Search Obsidian notes", 6 | "Author": "Garulf", 7 | "Version": "1.1.4", 8 | "Language": "python", 9 | "Website": "https://github.com/Garulf/obsidian-notes", 10 | "IcoPath": "./icon.png", 11 | "ExecuteFileName": "run.py" 12 | } -------------------------------------------------------------------------------- /plugin/main.py: -------------------------------------------------------------------------------- 1 | from difflib import SequenceMatcher as SM 2 | 3 | from flox import Flox 4 | 5 | import obsidian 6 | 7 | CHECK_BOX_GLYPH = '\ue003' 8 | MARKED_CHECK_BOX_GLYPH = '\ue005' 9 | 10 | 11 | def match(query, match): 12 | return int(SM(lambda x: x == " ", query.lower().replace('\\', ' '), match.lower().replace('\\', ' '), autojunk=False).ratio() * 100) 13 | 14 | class Obsidian(Flox): 15 | 16 | def query(self, query): 17 | try: 18 | vaults = obsidian.get_vaults() 19 | except FileNotFoundError: 20 | self.add_item( 21 | title='Obsidian not found', 22 | subtitle='Please install Obsidian', 23 | ) 24 | return 25 | for vault in vaults: 26 | for note in vault.notes(): 27 | title_score = match(query, note.title) 28 | subtitle_score = match(query, str(note.vault_path)) 29 | score = max(title_score, subtitle_score) 30 | if score > 20 or query == '': 31 | self.add_item( 32 | title=note.title, 33 | subtitle=str(note.vault_path), 34 | icon=self.icon, 35 | method=self.open_note, 36 | parameters=[vault.name, str(note.relative_path)], 37 | score=score, 38 | context=[vault.id, str(note.path), note.checklists()] 39 | ) 40 | 41 | def context_menu(self, data): 42 | vault_id = data[0] 43 | note_path = data[1] 44 | for checks in data[2]: 45 | self.add_item( 46 | title=checks['description'], 47 | subtitle=checks['title'], 48 | glyph=MARKED_CHECK_BOX_GLYPH if checks['checked'] else CHECK_BOX_GLYPH, 49 | method=self.toggle_checkbox, 50 | parameters=[vault_id, note_path, checks['raw']], 51 | dont_hide=True 52 | ) 53 | 54 | def toggle_checkbox(self, vault_id, note_path, raw): 55 | note = obsidian.get_note(vault_id, note_path) 56 | note.toggle_checkbox(raw) 57 | 58 | 59 | def open_note(self, vault_name, note_path): 60 | obsidian.open_note(vault_name, note_path) 61 | 62 | if __name__ == "__main__": 63 | Obsidian() 64 | -------------------------------------------------------------------------------- /plugin/obsidian.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from pathlib import Path 4 | import webbrowser 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | VAULTS_FILE = 'obsidian.json' 10 | VAULTS_PATH = Path(os.getenv('APPDATA'), 'obsidian', VAULTS_FILE) 11 | CHECK_BOX = '- [ ]' 12 | MARKED_CHECK_BOX = '- [x]' 13 | 14 | def get_vaults(): 15 | vaults = [] 16 | try: 17 | with open(VAULTS_PATH, 'r', encoding='utf-8', errors='replace') as f: 18 | data = json.load(f) 19 | except FileNotFoundError: 20 | logger.error(f'{VAULTS_PATH} not found!\nIs obsidian installed?') 21 | raise 22 | else: 23 | for vault in data['vaults'].keys(): 24 | vaults.append(Vault(vault, data['vaults'][vault])) 25 | return vaults 26 | 27 | def get_vault(id): 28 | try: 29 | with open(VAULTS_PATH, 'r', encoding='utf-8', errors='replace') as f: 30 | data = json.load(f) 31 | except FileNotFoundError: 32 | logger.error(f'{VAULTS_PATH} not found!\nIs obsidian installed?') 33 | raise 34 | else: 35 | try: 36 | return Vault(id, data['vaults'][id]) 37 | except KeyError: 38 | logger.error(f'{id} not found!') 39 | raise 40 | 41 | def get_note(vault_id, note_path): 42 | vault = get_vault(vault_id) 43 | return Note(vault, note_path) 44 | 45 | 46 | def open_note(vault_name, note_path): 47 | URI = f'open?vault={vault_name}&file={note_path}'.replace(' ', '%20').replace('/', '%2F').replace('\\', '%2F') 48 | URI = f'obsidian://{URI}' 49 | webbrowser.open(URI) 50 | 51 | class Vault(object): 52 | 53 | def __init__(self, id:str, vault: dict): 54 | self._data = vault 55 | self.id = id 56 | self.name = Path(vault['path']).name 57 | for key, value in vault.items(): 58 | setattr(self, key, value) 59 | 60 | def notes(self): 61 | notes = [] 62 | for note in Path(self.path).glob('**/*.md'): 63 | notes.append(Note(self, note)) 64 | return notes 65 | 66 | def note(self, note_path): 67 | for note in self.notes(): 68 | if str(note.relative_path) == note_path: 69 | return note 70 | 71 | 72 | class Note(object): 73 | 74 | def __init__(self, vault: Vault, full_path: str): 75 | self.vault = vault 76 | self.path = full_path 77 | self.title = Path(full_path).name.replace('.md', '') 78 | self.relative_path = Path(str(full_path).replace(f'{self.vault.path}', '')) 79 | self.vault_path = f'{self.vault.name}{self.relative_path}' 80 | 81 | def open_note(self): 82 | open_note(self.vault.name, self.relative_path) 83 | 84 | def content(self): 85 | with open(self.path, 'r', encoding='utf-8', errors='replace') as f: 86 | return f.read() 87 | 88 | 89 | def toggle_checkbox(self, raw): 90 | content = self.content() 91 | for line in content.splitlines(): 92 | if raw == line: 93 | if MARKED_CHECK_BOX in line: 94 | toggled_line = line.replace(MARKED_CHECK_BOX, CHECK_BOX) 95 | else: 96 | toggled_line = line.replace(CHECK_BOX, MARKED_CHECK_BOX) 97 | break 98 | content = content.replace(line, toggled_line) 99 | with open(self.path, 'w', encoding='utf-8', errors='replace') as f: 100 | f.write(content) 101 | 102 | def checklists(self): 103 | checklists = [] 104 | title = '' 105 | prev_line = '' 106 | for line in self.content().splitlines(): 107 | if CHECK_BOX in line or MARKED_CHECK_BOX in line: 108 | description = line.replace(CHECK_BOX, '').replace(MARKED_CHECK_BOX, '').strip() 109 | if MARKED_CHECK_BOX in line: 110 | checked = True 111 | else: 112 | checked = False 113 | if (CHECK_BOX not in prev_line and MARKED_CHECK_BOX not in prev_line) and prev_line.endswith(':'): 114 | title = prev_line.replace(':', '').strip() 115 | checklists.append( 116 | { 117 | 'title': title, 118 | 'description': description, 119 | 'checked': checked, 120 | 'raw': line 121 | } 122 | ) 123 | else: 124 | title = '' 125 | prev_line = line 126 | return checklists 127 | 128 | 129 | 130 | 131 | if __name__ == "__main__": 132 | vaults = get_vaults() 133 | for vault in vaults: 134 | for note in vault.notes(): 135 | print(note.title) 136 | open_note(vault.name, str(note.relative_path)) 137 | break -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flox-lib==0.18.1 2 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | plugindir = os.path.abspath(os.path.dirname(__file__)) 5 | sys.path.append(plugindir) 6 | sys.path.append(os.path.join(plugindir, "lib")) 7 | sys.path.append(os.path.join(plugindir, "plugin")) 8 | 9 | from plugin.main import Obsidian 10 | 11 | if __name__ == "__main__": 12 | Obsidian() --------------------------------------------------------------------------------