├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── main.ts ├── manifest.json ├── package.json ├── rollup.config.js ├── styles.css ├── tsconfig.json ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | main.js 11 | *.js.map 12 | 13 | # obsidian 14 | data.json 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaellee/stille/493a14198493cf24254e62ebe1f5b8c10b28c8d7/.prettierignore -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Michael Lee 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🌗 Stille 2 | 3 | An Obsidian plugin that helps you focus on your writing, a section at a time. 4 | 5 | ![A screenshot of Stille](https://user-images.githubusercontent.com/1329644/197059096-7a3ad259-6fc3-4471-8f0b-e28c551152eb.png) 6 | 7 | [Learn more about Stille.](https://michaelsoolee.com/obsidian-focus-plugin-stille/) 8 | 9 | ## Install Stille 10 | You can [click on this link](https://obsidian.md/plugins?id=obsidian-stille) and it will open up Obsidian and give you the option to install Stille. 11 | 12 | Or 13 | 14 | You can install Stille from within Obsidian by going to `Settings > Community plugins` and clicking the **Browse** button under *Community plugins*. Then search for *Stille* in the search field. 15 | 16 | ## Getting started 17 | - Once Stille is installed, make sure the plugin is activated under *Community plugins > Installed plugins* in *Settings* 18 | - Once activated, you'll see a new moon-shaped icon in the left hand ribbon. This is the toggle to turn Stille on and off. You'll also see in the bottom status bar, a status for Stille that indicates whether it's on or off. 19 | - Stille also comes with a hotkey to toggle it on or off, `command + shift + s` on macOS or `control + shift + s` on Windows. 20 | 21 | ## To develop 22 | 23 | - Clone repository 24 | - Run `npm i` to install dependencies 25 | - Run `npm run dev` 26 | 27 | ## To install from repo 28 | 29 | - Follow steps above in "To develop" section 30 | - Instead of running `npm run dev`, you want to run `npm run build` 31 | - Create a folder in your vault's `.obsidian/plugins` folder called "stille" 32 | - Then copy and paste the `main.js`, `manifest.json` and `styles.css` files into the new stille folder 33 | - Activate community plugins by turning off safe mode under settings and you should now see the option to turn on Stille 34 | 35 | ## Acknowledgement 36 | 37 | [Limelight by Junegunn Choi](https://github.com/junegunn/limelight.vim) 38 | 39 | ## Show gratitude 40 | If you've benefited from using [Stille for Obsidian](https://obsidian.md/plugins?id=obsidian-stille), please consider [buying me a slice of pizza](https://michaellee.gumroad.com/l/buy-michael-pizza) :pizza: 41 | 42 | It keeps me motivated to continue to support and improve Stille. 43 | 44 | ## License 45 | 46 | MIT 47 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from 'builtin-modules' 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === 'production'); 13 | 14 | esbuild.build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ['main.ts'], 19 | bundle: true, 20 | external: [ 21 | 'obsidian', 22 | 'electron', 23 | '@codemirror/autocomplete', 24 | '@codemirror/closebrackets', 25 | '@codemirror/collab', 26 | '@codemirror/commands', 27 | '@codemirror/comment', 28 | '@codemirror/fold', 29 | '@codemirror/gutter', 30 | '@codemirror/highlight', 31 | '@codemirror/history', 32 | '@codemirror/language', 33 | '@codemirror/lint', 34 | '@codemirror/matchbrackets', 35 | '@codemirror/panel', 36 | '@codemirror/rangeset', 37 | '@codemirror/rectangular-selection', 38 | '@codemirror/search', 39 | '@codemirror/state', 40 | '@codemirror/stream-parser', 41 | '@codemirror/text', 42 | '@codemirror/tooltip', 43 | '@codemirror/view', 44 | '@lezer/common', 45 | '@lezer/highlight', 46 | '@lezer/lr', 47 | ...builtins], 48 | format: 'cjs', 49 | watch: !prod, 50 | target: 'es2016', 51 | logLevel: "info", 52 | sourcemap: prod ? false : 'inline', 53 | treeShaking: true, 54 | outfile: 'main.js', 55 | }).catch(() => process.exit(1)); -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { App, Plugin, addIcon, PluginSettingTab, Setting } from "obsidian"; 2 | 3 | const nameOfApplication = `Stille`; 4 | 5 | interface StillePluginSettings { 6 | unfocusedLevel: number; 7 | statusBarLabel: boolean; 8 | statusBarLabelText: string; 9 | unfocusTitle: boolean; 10 | } 11 | 12 | // Default settings values 13 | const DEFAULT_SETTINGS: StillePluginSettings = { 14 | unfocusedLevel: 0.3, 15 | statusBarLabel: true, 16 | statusBarLabelText: `${nameOfApplication} on`, 17 | unfocusTitle: false, 18 | }; 19 | 20 | // Icon for the side dock ribbon toggle for Stille 21 | // Icon is from the ionicons icon set https://github.com/ionic-team/ionicons/blob/main/src/svg/moon-sharp.svg 22 | const moonIcon = ``; 23 | 24 | export default class StillePlugin extends Plugin { 25 | settings: StillePluginSettings; 26 | statusBar: HTMLElement; 27 | styleElement: HTMLElement; 28 | stilleStatus: boolean = false; 29 | 30 | async onload() { 31 | // Load settings values 32 | await this.loadSettings(); 33 | 34 | // Add Stille toggle to ribbon 35 | addIcon("moon", moonIcon); 36 | this.addRibbonIcon("moon", "Toggle Stille", () => { 37 | this.toggleStille(); 38 | }); 39 | 40 | // Add Stille status to status bar 41 | this.statusBar = this.addStatusBarItem(); 42 | this.statusBar.setText(this.settings.statusBarLabelText); 43 | this.toggleLabelDisplay(this.settings.statusBarLabel, true); 44 | 45 | this.toggleDimTitle(this.settings.unfocusTitle); 46 | 47 | // Add Stille toggle shortcut 48 | this.addCommand({ 49 | id: "toggleStille", 50 | name: "Toggle Stille", 51 | callback: () => { 52 | this.toggleStille(); 53 | }, 54 | hotkeys: [{ modifiers: ["Mod", "Shift"], key: "S" }], 55 | }); 56 | 57 | // Add Stille specific settings tab 58 | this.addSettingTab(new StilleSettingTab(this.app, this)); 59 | 60 | this.stilleStatus = true; 61 | 62 | this.toggleStille(); 63 | } 64 | 65 | async onunload() { 66 | this.removeStyleFromView(); 67 | this.statusBar.remove(); 68 | } 69 | 70 | async loadSettings() { 71 | this.settings = Object.assign( 72 | {}, 73 | DEFAULT_SETTINGS, 74 | await this.loadData() 75 | ); 76 | } 77 | 78 | async saveSettings() { 79 | await this.saveData(this.settings); 80 | } 81 | 82 | async toggleStille() { 83 | this.stilleStatus = !this.stilleStatus; 84 | 85 | if (this.stilleStatus) { 86 | this.addStyleToView(); 87 | this.settings.statusBarLabelText = `${nameOfApplication} on`; 88 | this.statusBar.setText(this.settings.statusBarLabelText); 89 | } else { 90 | this.removeStyleFromView(); 91 | this.settings.statusBarLabelText = `${nameOfApplication} off`; 92 | this.statusBar.setText(this.settings.statusBarLabelText); 93 | } 94 | } 95 | 96 | addStyleToView() { 97 | this.styleElement = document.createElement("style"); 98 | this.styleElement.id = "stilleStyles"; 99 | document.head.appendChild(this.styleElement); 100 | document.body.classList.add("StilleStyle"); 101 | const dimTitle = this.settings.unfocusTitle; 102 | if (dimTitle) { 103 | this.toggleDimTitle(dimTitle); 104 | } 105 | 106 | this.updateStyles(); 107 | } 108 | 109 | removeStyleFromView() { 110 | if (this.styleElement) { 111 | this.styleElement.remove(); 112 | document.body.removeClass("StilleStyle"); 113 | document.body.removeClass("StilleUnfocusTitle"); 114 | } 115 | } 116 | 117 | updateStyles() { 118 | this.styleElement.textContent = `body { 119 | --unfocusedLevel: ${this.settings.unfocusedLevel}; 120 | }`; 121 | } 122 | 123 | toggleLabelDisplay(value: boolean, pluginOnLoad?: boolean) { 124 | if (!value) { 125 | this.statusBar.remove(); 126 | } 127 | if (value && !pluginOnLoad) { 128 | this.statusBar = this.addStatusBarItem(); 129 | this.statusBar.setText(this.settings.statusBarLabelText); 130 | } 131 | } 132 | 133 | toggleDimTitle(value: boolean) { 134 | if (value) { 135 | document.body.classList.add("StilleUnfocusTitle"); 136 | } else { 137 | document.body.classList.remove("StilleUnfocusTitle"); 138 | } 139 | } 140 | 141 | async toggleLabel(value: boolean) { 142 | this.toggleLabelDisplay(value); 143 | this.settings.statusBarLabel = value; 144 | await this.saveSettings(); 145 | } 146 | 147 | async toggleTitle(value: boolean) { 148 | this.toggleDimTitle(value); 149 | this.settings.unfocusTitle = value; 150 | await this.saveSettings(); 151 | } 152 | 153 | refresh() { 154 | this.removeStyleFromView(); 155 | this.addStyleToView(); 156 | } 157 | } 158 | 159 | class StilleSettingTab extends PluginSettingTab { 160 | plugin: StillePlugin; 161 | 162 | constructor(app: App, plugin: StillePlugin) { 163 | super(app, plugin); 164 | this.plugin = plugin; 165 | } 166 | 167 | display(): void { 168 | const { containerEl } = this; 169 | containerEl.empty(); 170 | containerEl.createEl("h3", { 171 | text: `${nameOfApplication} — Focus on your writing.`, 172 | }); 173 | containerEl.createEl("h4", { 174 | text: "v" + this.plugin.manifest.version, 175 | }); 176 | containerEl.createEl("a", { 177 | text: `Learn more about ${nameOfApplication}`, 178 | href: "https://michaelsoolee.com/obsidian-focus-plugin-stille/", 179 | }); 180 | containerEl.createEl("br"); 181 | containerEl.createEl("span", { 182 | text: `If ${nameOfApplication} has helped you focus, consider buying me a slice of pizza 🍕 `, 183 | }); 184 | containerEl.createEl("a", { 185 | text: "Buy Michael, a slice of pizza", 186 | href: "https://michaellee.gumroad.com/l/buy-michael-pizza", 187 | }); 188 | containerEl.createEl("br"); 189 | containerEl.createEl("br"); 190 | // Adds setting to control opacity of unfocused test 191 | // The uses an input field and will update the unfocusedLevel settings value 192 | new Setting(containerEl) 193 | .setName("Opacity level for unfocused text") 194 | .setDesc( 195 | "This is the opacity level for text that is unfocused. This value should be a decimal value from 0.0 to 1.0." 196 | ) 197 | .addText((text) => 198 | text 199 | .setPlaceholder("A value from 0.0 to 1.0") 200 | .setValue(this.plugin.settings.unfocusedLevel + "") 201 | .onChange(async (value) => { 202 | this.plugin.settings.unfocusedLevel = Number(value); 203 | await this.plugin.saveSettings(); 204 | this.plugin.refresh(); 205 | }) 206 | ); 207 | 208 | // Adds settings option to toggle Stille's status bar label 209 | // This provides a toggle which controls the settings value for the statusBarLabel 210 | new Setting(containerEl) 211 | .setName("Toggle status bar label") 212 | .setDesc( 213 | `Use this to toggle the visibility of the status bar label for ${nameOfApplication}.` 214 | ) 215 | .addToggle((barStatus) => 216 | barStatus 217 | .setValue(this.plugin.settings.statusBarLabel) 218 | .onChange(async () => { 219 | await this.plugin.toggleLabel( 220 | !this.plugin.settings.statusBarLabel 221 | ); 222 | }) 223 | ); 224 | 225 | // Adds settings option to toggle title to be dimmed 226 | // This provides a toggle which controls the settings value for the statusBarLabel 227 | new Setting(containerEl) 228 | .setName("Dim title") 229 | .setDesc( 230 | `Dim the title of the note when ${nameOfApplication} is on.` 231 | ) 232 | .addToggle((title) => 233 | title 234 | .setValue(this.plugin.settings.unfocusTitle) 235 | .onChange(async () => { 236 | await this.plugin.toggleTitle( 237 | !this.plugin.settings.unfocusTitle 238 | ); 239 | }) 240 | ); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-stille", 3 | "name": "Stille", 4 | "version": "1.3.4", 5 | "minAppVersion": "0.15.0", 6 | "description": "Focus on your writing, a section at a time.", 7 | "author": "Michael Lee", 8 | "authorUrl": "https://michaelsoolee.com", 9 | "isDesktopOnly": false, 10 | "fundingUrl": { 11 | "Buy me a slice of pizza": "https://michaellee.gumroad.com/l/buy-michael-pizza" 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-stille", 3 | "version": "1.3.4", 4 | "description": "An Obsidian plugin that helps you focus on your writing, a section at a time.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@typescript-eslint/eslint-plugin": "^5.2.0", 17 | "@typescript-eslint/parser": "^5.2.0", 18 | "builtin-modules": "^3.2.0", 19 | "esbuild": "0.13.12", 20 | "obsidian": "^0.12.17", 21 | "prettier": "2.7.1", 22 | "tslib": "2.3.1", 23 | "typescript": "4.4.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | 5 | const isProd = (process.env.BUILD === 'production'); 6 | 7 | const banner = 8 | `/* 9 | THIS IS A GENERATED/BUNDLED FILE BY ROLLUP 10 | if you want to view the source visit the plugins github repository 11 | */ 12 | `; 13 | 14 | export default { 15 | input: 'main.ts', 16 | output: { 17 | dir: '.', 18 | sourcemap: 'inline', 19 | sourcemapExcludeSources: isProd, 20 | format: 'cjs', 21 | exports: 'default', 22 | banner, 23 | }, 24 | external: ['obsidian'], 25 | plugins: [ 26 | typescript(), 27 | nodeResolve({browser: true}), 28 | commonjs(), 29 | ] 30 | }; -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* Any line that isn't focused on, reduce opacity */ 2 | body.StilleStyle .cm-editor.cm-focused .cm-line:not(.cm-active) { 3 | opacity: var(--unfocusedLevel); 4 | } 5 | 6 | /* If the editor does not have focus, then reduce opacity of contents of editor */ 7 | body.StilleStyle .cm-editor:not(.cm-focused) .cm-line { 8 | opacity: var(--unfocusedLevel); 9 | } 10 | 11 | body.StilleStyle .cm-editor .metadata-container { 12 | opacity:var(--unfocusedLevel); 13 | } 14 | 15 | body.StilleStyle .cm-editor .internal-embed, 16 | body.StilleStyle .cm-editor .cm-embed-block { 17 | opacity: var(--unfocusedLevel); 18 | } 19 | 20 | body.StilleStyle .cm-editor .cm-active { 21 | opacity: 1; 22 | } 23 | 24 | body.StilleStyle.is-mobile .cm-editor .cm-active { 25 | opacity: 1; 26 | } 27 | 28 | body.StilleUnfocusTitle .view-header-title { 29 | opacity: var(--unfocusedLevel); 30 | } 31 | 32 | body.StilleUnfocusTitle .view-header-title:focus-within { 33 | opacity: 1; 34 | } 35 | 36 | /* Below are v1.0 specific fixes */ 37 | 38 | body.StilleUnfocusTitle .inline-title { 39 | opacity: var(--unfocusedLevel); 40 | } 41 | 42 | body.StilleUnfocusTitle .inline-title:focus-within { 43 | opacity: 1; 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7" 19 | ] 20 | }, 21 | "include": [ 22 | "**/*.ts" 23 | ] 24 | } -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.9.12", 3 | "1.0.1": "0.9.12", 4 | "1.0.2": "0.9.12", 5 | "1.0.3": "0.9.12", 6 | "1.1.0": "0.13.0", 7 | "1.2.0": "0.13.0", 8 | "1.2.1": "0.13.0", 9 | "1.3.0": "0.15.0", 10 | "1.3.1": "0.15.0", 11 | "1.3.2": "0.15.0", 12 | "1.3.3": "0.15.0", 13 | "1.3.4": "0.15.0" 14 | } --------------------------------------------------------------------------------