Slight refactor, add support for exact & glob matchers

This commit is contained in:
Lucy Dryaeva 2026-01-31 17:03:20 +00:00
parent a8e1e3d09f
commit dc5bdd8609
9 changed files with 235 additions and 57 deletions

View file

@ -40,5 +40,8 @@
"tslib": "2.4.0",
"typescript": "5.8.3",
"typescript-eslint": "^8.29.1"
},
"dependencies": {
"minimatch": "^10.1.1"
}
}

View file

@ -7,6 +7,10 @@ settings:
importers:
.:
dependencies:
minimatch:
specifier: ^10.1.1
version: 10.1.1
devDependencies:
'@eslint/js':
specifier: ^9.24.0
@ -287,6 +291,14 @@ packages:
resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==}
engines: {node: '>=18.18'}
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
'@isaacs/brace-expansion@5.0.0':
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
engines: {node: 20 || >=22}
'@jridgewell/gen-mapping@0.3.8':
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
engines: {node: '>=6.0.0'}
@ -435,36 +447,43 @@ packages:
resolution: {integrity: sha512-FX2FV7vpLE/+Z0NZX9/1pwWud5Wocm/2PgpUXbT5aSV3QEB10kBPJAzssOQylvdj8mOHoKl5pVkXpbCwww/T2g==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.5.0':
resolution: {integrity: sha512-+gF97xst1BZb28T3nwwzEtq2ewCoMDGKsenYsZuvpmNrW0019G1iUAunZN+FG55L21y+uP7zsGX06OXDQ/viKw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.5.0':
resolution: {integrity: sha512-5bEmVcQw9js8JYM2LkUBw5SeELSIxX+qKf9bFrfFINKAp4noZ//hUxLpbF7u/3gTBN1GsER6xOzIZlw/VTdXtA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.5.0':
resolution: {integrity: sha512-GGk/8TPUsf1Q99F+lzMdjE6sGL26uJCwQ9TlvBs8zR3cLQNw/MIumPN7zrs3GFGySjnwXc8gA6J3HKbejywmqA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-s390x-gnu@1.5.0':
resolution: {integrity: sha512-5uRkFYYVNAeVaA4W/CwugjFN3iDOHCPqsBLCCOoJiMfFMMz4evBRsg+498OFa9w6VcTn2bD5aI+RRayaIgk2Sw==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.5.0':
resolution: {integrity: sha512-j905CZH3nehYy6NimNqC2B14pxn4Ltd7guKMyPTzKehbFXTUgihQS/ZfHQTdojkMzbSwBOSgq1dOrY+IpgxDsA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.5.0':
resolution: {integrity: sha512-dmLevQTuzQRwu5A+mvj54R5aye5I4PVKiWqGxg8tTaYP2k2oTs/3Mo8mgnhPk28VoYCi0fdFYpgzCd4AJndQvQ==}
cpu: [x64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.5.0':
resolution: {integrity: sha512-LtJMhwu7avhoi+kKfAZOKN773RtzLBVVF90YJbB0wyMpUj9yQPeA+mteVUI9P70OG/opH47FeV5AWeaNWWgqJg==}
@ -853,8 +872,8 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
minimatch@10.0.1:
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
minimatch@10.1.1:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
minimatch@3.1.2:
@ -1328,6 +1347,12 @@ snapshots:
'@humanwhocodes/retry@0.4.2': {}
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
dependencies:
'@isaacs/balanced-match': 4.0.1
'@jridgewell/gen-mapping@0.3.8':
dependencies:
'@jridgewell/set-array': 1.2.1
@ -1685,7 +1710,7 @@ snapshots:
eslint-import-resolver-node: 0.3.9
get-tsconfig: 4.10.0
is-glob: 4.0.3
minimatch: 10.0.1
minimatch: 10.1.1
semver: 7.7.1
stable-hash: 0.0.5
tslib: 2.8.1
@ -1925,9 +1950,9 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
minimatch@10.0.1:
minimatch@10.1.1:
dependencies:
brace-expansion: 2.0.1
'@isaacs/brace-expansion': 5.0.0
minimatch@3.1.2:
dependencies:

View file

@ -1,21 +0,0 @@
<script lang="ts">
import type { App } from 'obsidian'
import { PathSuggestions } from 'src/settings'
import { onMount } from 'svelte'
let inputEl: HTMLInputElement
onMount(() => {
new PathSuggestions(app, inputEl).onSelect((v) => (path = v))
})
interface Props {
app: App
path: string
disabled: boolean
}
let { app, path = $bindable(''), disabled }: Props = $props()
</script>
<input type="text" bind:this={inputEl} placeholder="Path" bind:value={path} {disabled} />

View file

@ -1,10 +1,11 @@
<script lang="ts">
import type { App } from 'obsidian'
import Path from './Path.svelte'
import Path from './path/SinglePath.svelte'
import type { Writable, Readable } from 'svelte/store'
import { ProtectedPathType, type ProtectedPath } from 'src/settings'
interface Props {
paths: Writable<string[]>
paths: Writable<ProtectedPath[]>
app: App
disabled: Readable<boolean>
}
@ -29,18 +30,33 @@
<hr />
<h3>Protected paths</h3>
<div class="setting-item-description">
These are the actual files, folders or patterns you want to protect. <br />
For protecting entire folders, make sure there is a '/' at the end. <br />
<a
aria-label="Glob Matching Guide Link"
href="https://www.malikbrowne.com/blog/a-beginners-guide-glob-patterns/"
rel="noopener nofollow"
class="external-link"
target="_blank">Rules for globs can be learn here</a
>
</div>
<div class="sp-paths">
{#each $paths as _, i (i)}
<div class="sp-path-container">
<Path {app} bind:path={$paths[i]} disabled={$disabled} />
<Path
{app}
bind:content={$paths[i].content}
bind:type={$paths[i].type}
disabled={$disabled}
/>
<button
disabled={$disabled}
class="mod-err"
aria-label="Remove path"
{...evWrapper(() => {
$paths.splice(i, 1)
$paths = $paths
})}
>
@ -53,7 +69,9 @@
disabled={$disabled}
class="mod-cta"
aria-label="Add new path"
{...evWrapper(() => ($paths = [...$paths, '/']))}
{...evWrapper(
() => ($paths = [...$paths, { type: ProtectedPathType.PREFIX, content: '/' }])
)}
>
+
</button>
@ -61,6 +79,7 @@
<style>
.sp-paths {
margin-top: var(--size-4-2);
display: flex;
flex-direction: column;
gap: var(--size-4-2);

View file

@ -0,0 +1,87 @@
<script lang="ts">
import { AbstractInputSuggest, App } from 'obsidian'
import { ProtectedPathType } from 'src/settings'
import { normalizePath } from 'src/utils'
import { onMount } from 'svelte'
interface Props {
app: App
type: ProtectedPathType
content: string
disabled: boolean
}
let { content = $bindable(), type, app, disabled }: Props = $props()
class PathSuggester extends AbstractInputSuggest<string> {
filesOnly: boolean
protected getSuggestions(query: string): string[] {
const p = normalizePath(query)
let paths = []
if (this.filesOnly) {
paths = this.app.vault.getFiles().map((f) => {
return f.path
})
} else {
paths = this.app.vault.getAllFolders(true).map((f) => {
return f.path
})
}
return paths.filter((v) => {
const sug = '/' + v
if (sug.startsWith(p)) return true
if (p.endsWith('/')) return sug == p.slice(0, -1)
return false
})
}
renderSuggestion(value: string, el: HTMLElement): void {
el.setText(value)
}
}
let inputEl: HTMLInputElement
let activeElement = $state<HTMLElement>()
onMount(() => {
if (type == ProtectedPathType.GLOB) {
return
}
const suggester = new PathSuggester(app, inputEl).onSelect((v) => {
content = "/" + v
if (type == ProtectedPathType.PREFIX) {
content += '/'
}
suggester.close()
})
suggester.filesOnly = type == ProtectedPathType.EXACT
})
// $effect(() => {
// if (disabled || activeElement != inputEl || type == ProtectedPathType.GLOB) {
// console.log("Closing...")
// suggester?.close()
// return
// }
// if (!suggester) {
// suggester = new PathSuggester(app, inputEl).onSelect((v) => {
// content = "/" + v
// if (type == ProtectedPathType.PREFIX) {
// content += '/'
// }
// suggester.close()
// })
// suggester.filesOnly = type == ProtectedPathType.EXACT
// }
// suggester.open()
// })
</script>
<input type="text" bind:this={inputEl} placeholder="Matcher" bind:value={content} {disabled} />
<svelte:document bind:activeElement={activeElement} />

View file

@ -0,0 +1,29 @@
<script lang="ts">
import type { App } from 'obsidian'
import { ProtectedPathType } from 'src/settings'
import PathWithSuggestion from './PathWithSuggestion.svelte'
interface Props {
app: App
content: string,
type: ProtectedPathType,
disabled: boolean
}
let {
app,
content = $bindable<string>(),
type = $bindable<ProtectedPathType>(),
disabled
}: Props = $props()
</script>
<select class="dropdown" bind:value={type} {disabled}>
<option value={ProtectedPathType.PREFIX}>Prefix Match (Folders)</option>
<option value={ProtectedPathType.EXACT}>Exact File</option>
<option value={ProtectedPathType.GLOB}>Glob Matching</option>
</select>
{#key type}
<PathWithSuggestion {app} bind:content {disabled} {type} />
{/key}

View file

@ -2,9 +2,10 @@ import type { WorkspaceLeaf } from 'obsidian'
import { type TFile } from 'obsidian'
import { FileView, Notice, Plugin, setIcon } from 'obsidian'
import type { Settings } from './settings'
import { DEFAULT_SETTINGS, PrivacyMode, SettingsTab } from './settings'
import { DEFAULT_SETTINGS, PrivacyMode, ProtectedPathType, SettingsTab } from './settings'
import { RequirePasswordModal } from './modals'
import { modifySearchLeaf } from './search'
import { normalizePath, pathComparer } from './utils'
const STR_RIBBON_LOCKED = 'Vault is locked'
const STR_RIBBON_UNLOCKED = 'Vault is unlocked'
@ -124,10 +125,9 @@ export default class SimplePassword extends Plugin {
}
isPathLocked(testPath: string): boolean {
for (const _p of this.settings.protectedPaths) {
const p = decodeURIComponent(new URL(_p, 'file://').pathname)
if (('/' + testPath).startsWith(p)) {
testPath = normalizePath(testPath)
for (const pp of this.settings.protectedPaths) {
if (pathComparer[pp.type](pp.content, testPath)) {
return true
}
}
@ -263,6 +263,27 @@ export default class SimplePassword extends Plugin {
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData())
await this.migrateSettings()
}
async migrateSettings() {
let changed = false
if (this.settings.protectedPaths.length != 0) {
this.settings.protectedPaths = this.settings.protectedPaths.map(v => {
if (typeof v == "string") {
changed = true
return {
type: ProtectedPathType.PREFIX,
content: v
}
}
return v
})
}
if (changed) {
await this.saveSettings()
}
}
async saveSettings() {

View file

@ -12,19 +12,35 @@ export enum PrivacyMode {
NONE = ''
}
export enum ProtectedPathType {
PREFIX = 'prefix',
EXACT = 'exact',
GLOB = 'glob',
}
export interface ProtectedPath {
type: ProtectedPathType
content: string
}
export interface Settings {
autoLockMinutes: number
privacyMode: PrivacyMode
protectedPaths: string[]
hint: string
password: string
blockGraph: boolean
protectedPaths: ProtectedPath[]
}
export const DEFAULT_SETTINGS: Settings = {
autoLockMinutes: 15,
privacyMode: PrivacyMode.BLUR,
protectedPaths: ['/'],
protectedPaths: [
{
type: ProtectedPathType.PREFIX,
content: '/'
}
],
hint: '',
password: '',
blockGraph: true
@ -214,22 +230,3 @@ export class SettingsTab extends PluginSettingTab {
super.hide()
}
}
export class PathSuggestions extends AbstractInputSuggest<string> {
protected getSuggestions(query: string): string[] {
const p = new URL(query, 'file://').pathname
return this.app.vault
.getAllFolders(true)
.filter((v) => {
if (('/' + v.path).startsWith(p)) {
return true
}
})
.map((v) => v.path)
}
renderSuggestion(value: string, el: HTMLElement): void {
el.setText(value)
}
}

18
src/utils.ts Normal file
View file

@ -0,0 +1,18 @@
import { minimatch } from "minimatch"
import { ProtectedPathType } from "./settings"
export function normalizePath(str: string) {
return decodeURIComponent(new URL(str, 'file://').pathname)
}
export const pathComparer: Record<ProtectedPathType, (content: string, realPath: string) => boolean> = {
[ProtectedPathType.PREFIX]: (content, realPath) => {
return realPath.startsWith(content)
},
[ProtectedPathType.EXACT]: (content, realPath) => {
return normalizePath(content) == normalizePath(realPath)
},
[ProtectedPathType.GLOB]: (content, realPath) => {
return minimatch(realPath, content)
}
}