@ -0,0 +1 @@ | 
				
			|||
.fdignore | 
				
			|||
@ -1,318 +0,0 @@ | 
				
			|||
let p = console.log | 
				
			|||
import { err } from './utils' | 
				
			|||
 | 
				
			|||
import db from './db' | 
				
			|||
import state from './state' | 
				
			|||
 | 
				
			|||
import config from './config' | 
				
			|||
import { find, omit, orderBy } from 'lodash' | 
				
			|||
import { nanoid } from 'nanoid' | 
				
			|||
import fzi from 'fzi' | 
				
			|||
import { evaluate as eval_math } from 'mathjs' | 
				
			|||
import { cloneDeep } from 'lodash' | 
				
			|||
 | 
				
			|||
export default new class api | 
				
			|||
 | 
				
			|||
	def add_link text | 
				
			|||
		let link = await create_link_from_text text | 
				
			|||
		link.id = nanoid! | 
				
			|||
		await db.links.add link | 
				
			|||
		await reload_db! | 
				
			|||
		imba.commit! | 
				
			|||
		p omit(link, "icon") | 
				
			|||
		return link | 
				
			|||
 | 
				
			|||
	def update_link old_link, new_link_text | 
				
			|||
		let new_link = await create_link_from_text new_link_text | 
				
			|||
		new_link.frequency = old_link.frequency | 
				
			|||
		let result = await db.links.update old_link.id, new_link | 
				
			|||
		throw "Link id not found." if result is 0 | 
				
			|||
		await reload_db! | 
				
			|||
		imba.commit! | 
				
			|||
		p omit(old_link, "icon") | 
				
			|||
		p omit(new_link, "icon") | 
				
			|||
		return new_link | 
				
			|||
 | 
				
			|||
	def put_link link | 
				
			|||
		try | 
				
			|||
			await db.links.update link.id, link | 
				
			|||
			if link.is_bang and config.data.default_bang.id is link.id | 
				
			|||
				config.set_default_bang link | 
				
			|||
			await reload_db! | 
				
			|||
		catch e | 
				
			|||
			err "putting link", e | 
				
			|||
 | 
				
			|||
	def delete_link link | 
				
			|||
		def go | 
				
			|||
			try | 
				
			|||
				await db.links.delete(link.id) | 
				
			|||
			catch e | 
				
			|||
				return err "deleting link", e | 
				
			|||
			try | 
				
			|||
				await reload_db! | 
				
			|||
			catch e | 
				
			|||
				return err "reloading db after successful delete", e | 
				
			|||
		state.loading = yes | 
				
			|||
		await go! | 
				
			|||
		state.link_selection_index = Math.min state.link_selection_index, state.sorted_links.length - 1 | 
				
			|||
		state.loading = no | 
				
			|||
 | 
				
			|||
	def pin_link link | 
				
			|||
		link.is_pinned = !link.is_pinned | 
				
			|||
		try | 
				
			|||
			let result = await db.links.update link.id, link | 
				
			|||
			throw "Link id not found." if result is 0 | 
				
			|||
		catch e | 
				
			|||
			return err "pinning link", e | 
				
			|||
		await reload_db! | 
				
			|||
		imba.commit! | 
				
			|||
 | 
				
			|||
	def reload_db | 
				
			|||
		state.links = await db.links.toArray() | 
				
			|||
		if state.active_bang | 
				
			|||
			let id = state.active_bang.id | 
				
			|||
			state.active_bang = find state.links, { id } | 
				
			|||
		let id = config.data.default_bang.id | 
				
			|||
		let link = find state.links, { id } | 
				
			|||
		if link | 
				
			|||
			config.data.default_bang = link | 
				
			|||
			config.save! | 
				
			|||
		sort_links! | 
				
			|||
 | 
				
			|||
	def increment_link_frequency link | 
				
			|||
		link.frequency += 1 | 
				
			|||
		try | 
				
			|||
			await put_link link | 
				
			|||
		catch e | 
				
			|||
			err "putting link", e | 
				
			|||
 | 
				
			|||
	def sort_links | 
				
			|||
		if state.query.trim!.length <= 0 | 
				
			|||
			state.sorted_links = orderBy(state.links, ['is_pinned', 'frequency'], ['desc', 'desc']) | 
				
			|||
		elif config.data.enable_effective_names | 
				
			|||
			state.sorted_links = fzi.sort state.query, state.links, do |x| x.name | 
				
			|||
		else | 
				
			|||
			state.sorted_links = fzi.sort state.query, state.links, do |x| x.display_name | 
				
			|||
 | 
				
			|||
	def name_exists new_name | 
				
			|||
		state.links.some! do |{name}| new_name is name | 
				
			|||
 | 
				
			|||
	def add_initial_links | 
				
			|||
		let initial_links = [ | 
				
			|||
			"tutorial github.com/familyfriendlymikey/fuzzyhome" | 
				
			|||
			"!brave search `b search.brave.com/search?q=" | 
				
			|||
			"!youtube youtube.com/results?search_query=" | 
				
			|||
			"photopea photopea.com" | 
				
			|||
			"twitch twitch.tv" | 
				
			|||
			"messenger `me messenger.com" | 
				
			|||
			"instagram `in instagram.com" | 
				
			|||
			"localhost `3000 http://localhost:3000" | 
				
			|||
		] | 
				
			|||
		for link_text in initial_links | 
				
			|||
			try | 
				
			|||
				add_link link_text | 
				
			|||
			catch e | 
				
			|||
				err "adding link", e | 
				
			|||
 | 
				
			|||
	def create_link_from_text text, get_icon=yes | 
				
			|||
		text = text.trim! | 
				
			|||
		throw "Text is empty." if text is '' | 
				
			|||
		let split_text = text.split(/\s+/) | 
				
			|||
		throw "No url provided." if split_text.length < 2 | 
				
			|||
		let url = split_text.pop! | 
				
			|||
		let host | 
				
			|||
		{ href:url, host } = parse_url url | 
				
			|||
		let name | 
				
			|||
		if split_text[-1].startsWith "`" | 
				
			|||
			name = split_text.pop!.slice(1) | 
				
			|||
		let display_name = split_text.join(" ") | 
				
			|||
		let is_bang = no | 
				
			|||
		let is_pinned = no | 
				
			|||
		if display_name.startsWith "!" | 
				
			|||
			is_bang = yes | 
				
			|||
			display_name = display_name.slice(1) | 
				
			|||
		name ||= display_name | 
				
			|||
		let link = { name, display_name, is_bang, is_pinned, url, frequency:0, history:[] } | 
				
			|||
		if get_icon | 
				
			|||
			link.icon = await fetch_image_as_base_64 host | 
				
			|||
		return link | 
				
			|||
 | 
				
			|||
	def fetch_image_as_base_64 host | 
				
			|||
		let fallback = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAH0lEQVR42mO8seXffwYqAsZRA0cNHDVw1MBRA0eqgQCDRkbJSQHxEQAAAABJRU5ErkJggg==' | 
				
			|||
		return new Promise! do |resolve| | 
				
			|||
			let res | 
				
			|||
			try | 
				
			|||
				res = await global.fetch("https://icon.horse/icon/{host}") | 
				
			|||
			catch | 
				
			|||
				p "Failed to get icon from icon horse." | 
				
			|||
				return resolve fallback | 
				
			|||
			# todo: can i use .text() on this or something | 
				
			|||
			let blob = await res.blob! | 
				
			|||
			let reader = new FileReader! | 
				
			|||
			reader.onload = do | 
				
			|||
				resolve this.result | 
				
			|||
			reader.onerror = do | 
				
			|||
				p "Failed to get data from reader." | 
				
			|||
				resolve fallback | 
				
			|||
				return | 
				
			|||
			reader.readAsDataURL(blob) | 
				
			|||
 | 
				
			|||
	def toggle_effective_names | 
				
			|||
		config.data.enable_effective_names = !config.data.enable_effective_names | 
				
			|||
		config.save! | 
				
			|||
		sort_links! | 
				
			|||
 | 
				
			|||
	def construct_link_text link | 
				
			|||
		link.display_name = link.display_name.trim! | 
				
			|||
		link.name = link.name.trim! | 
				
			|||
		link.url = link.url.trim! | 
				
			|||
		let link_text = "" | 
				
			|||
		link_text += "!" if link.is_bang | 
				
			|||
		link_text += link.display_name | 
				
			|||
		link_text += " `{link.name}" if link.name isnt link.display_name | 
				
			|||
		link_text += " {link.url}" | 
				
			|||
		link_text | 
				
			|||
 | 
				
			|||
	def parse_url url | 
				
			|||
		throw "invalid url" if url === null | 
				
			|||
		let get_url = do |s| | 
				
			|||
			let url = new URL s | 
				
			|||
			throw _ unless (url.host and url.href) | 
				
			|||
			url | 
				
			|||
		try | 
				
			|||
			return get_url url | 
				
			|||
		try | 
				
			|||
			return get_url "https://{url}" | 
				
			|||
		throw "invalid url" | 
				
			|||
 | 
				
			|||
	def get_pretty_date | 
				
			|||
		Date!.toString!.split(" ").slice(0, 4).join(" ") | 
				
			|||
 | 
				
			|||
	get selected_link | 
				
			|||
		state.sorted_links[state.link_selection_index] | 
				
			|||
 | 
				
			|||
	def set_link_selection_index index | 
				
			|||
		state.link_selection_index = index | 
				
			|||
 | 
				
			|||
	def increment_link_selection_index | 
				
			|||
		set_link_selection_index Math.min(state.sorted_links.length - 1, state.link_selection_index + 1) | 
				
			|||
 | 
				
			|||
	def decrement_link_selection_index | 
				
			|||
		set_link_selection_index Math.max(0, state.link_selection_index - 1) | 
				
			|||
 | 
				
			|||
	def navigate link | 
				
			|||
		await increment_link_frequency link | 
				
			|||
		window.location.href = link.url | 
				
			|||
 | 
				
			|||
	get math_result | 
				
			|||
		try | 
				
			|||
			let result = Number(eval_math state.query) | 
				
			|||
			throw _ if isNaN result | 
				
			|||
			throw _ if result.toString! is state.query.trim! | 
				
			|||
			result | 
				
			|||
		catch | 
				
			|||
			no | 
				
			|||
 | 
				
			|||
	def handle_cut e | 
				
			|||
		return unless e.target.selectionStart == e.target.selectionEnd | 
				
			|||
		let s = math_result | 
				
			|||
		s ||= state.query | 
				
			|||
		await window.navigator.clipboard.writeText(s) | 
				
			|||
		state.query = '' | 
				
			|||
		sort_links! | 
				
			|||
 | 
				
			|||
	def handle_add_link | 
				
			|||
		state.loading = yes | 
				
			|||
		try | 
				
			|||
			await add_link state.query | 
				
			|||
			state.query = '' | 
				
			|||
			sort_links! | 
				
			|||
		catch e | 
				
			|||
			err "adding link", e | 
				
			|||
		state.loading = no | 
				
			|||
 | 
				
			|||
	def handle_click_link | 
				
			|||
		let link = selected_link | 
				
			|||
		if link.is_bang | 
				
			|||
			state.query = '' | 
				
			|||
			state.active_bang = link | 
				
			|||
		else | 
				
			|||
			navigate link | 
				
			|||
 | 
				
			|||
	get bang | 
				
			|||
		state.active_bang or config.data.default_bang | 
				
			|||
 | 
				
			|||
	get encoded_bang_query | 
				
			|||
		"{bang.url}{window.encodeURIComponent(state.query)}" | 
				
			|||
 | 
				
			|||
	get encoded_bang_query_nourl | 
				
			|||
		"{window.encodeURIComponent(state.query)}" | 
				
			|||
 | 
				
			|||
	def update_history bang | 
				
			|||
		let text | 
				
			|||
		if state.bang_selection_index > -1 | 
				
			|||
			text = sorted_bang_history.splice(state.bang_selection_index, 1)[0] | 
				
			|||
		text ||= state.query.trim! | 
				
			|||
		return unless text | 
				
			|||
		let i = bang.history.indexOf(text) | 
				
			|||
		if i > -1 | 
				
			|||
			bang.history.splice(i, 1) | 
				
			|||
		bang.history.unshift text | 
				
			|||
		try | 
				
			|||
			await put_link bang | 
				
			|||
		catch e | 
				
			|||
			err "updating bang history", e | 
				
			|||
 | 
				
			|||
	def delete_bang_history_item | 
				
			|||
		let text = sorted_bang_history[state.bang_selection_index] | 
				
			|||
		return unless text | 
				
			|||
		let i = bang.history.indexOf(text) | 
				
			|||
		return unless i > -1 | 
				
			|||
		bang.history.splice(i, 1) | 
				
			|||
		try | 
				
			|||
			await put_link bang | 
				
			|||
			state.bang_selection_index = Math.min state.bang_selection_index, sorted_bang_history.length - 1 | 
				
			|||
		catch e | 
				
			|||
			err "updating bang history", e | 
				
			|||
 | 
				
			|||
	def handle_bang | 
				
			|||
		return if state.loading | 
				
			|||
		if state.bang_selection_index > -1 | 
				
			|||
			state.query = sorted_bang_history[state.bang_selection_index] | 
				
			|||
			state.bang_selection_index = -1 | 
				
			|||
			return | 
				
			|||
		await increment_link_frequency bang | 
				
			|||
		await update_history bang | 
				
			|||
		window.location.href = encoded_bang_query | 
				
			|||
 | 
				
			|||
	def unset_active_bang | 
				
			|||
		state.active_bang = no | 
				
			|||
		sort_links! | 
				
			|||
 | 
				
			|||
	def increment_bang_selection_index | 
				
			|||
		state.bang_selection_index = Math.min(sorted_bang_history.length - 1, state.bang_selection_index + 1) | 
				
			|||
 | 
				
			|||
	def decrement_bang_selection_index | 
				
			|||
		state.bang_selection_index = Math.max(-1, state.bang_selection_index - 1) | 
				
			|||
 | 
				
			|||
	get sorted_bang_history | 
				
			|||
		fzi.sort state.query, bang.history | 
				
			|||
 | 
				
			|||
	def delete_bang_history | 
				
			|||
		bang.history = [] | 
				
			|||
		try | 
				
			|||
			await put_link bang | 
				
			|||
			state.bang_selection_index = -1 | 
				
			|||
		catch e | 
				
			|||
			err "deleting bang history", e | 
				
			|||
		config.data.default_bang.history = [] | 
				
			|||
		config.save! | 
				
			|||
 | 
				
			|||
	def delete_all_bang_history | 
				
			|||
		return unless window.confirm "Are you sure you want to delete all bang history?" | 
				
			|||
		try | 
				
			|||
			await db.links.toCollection!.modify! do |link| link.history = [] | 
				
			|||
			await reload_db! | 
				
			|||
		catch e | 
				
			|||
			err "deleting some link histories", e | 
				
			|||
		imba.commit! | 
				
			|||
@ -1,14 +0,0 @@ | 
				
			|||
export default """ | 
				
			|||
!google search google.com/search?q= | 
				
			|||
!youtube search youtube.com/results?search_query= | 
				
			|||
!brave search search.brave.com/search?q= | 
				
			|||
!amazon search amazon.com/s?k= | 
				
			|||
!google site:reddit google.com/search?q=site%3Areddit.com%20 | 
				
			|||
!google site:stackoverflow google.com/search?q=site%3Astackoverflow.com%20 | 
				
			|||
twitch twitch.tv | 
				
			|||
instagram instagram.com | 
				
			|||
messenger messenger.com | 
				
			|||
photopea photopea.com | 
				
			|||
youtube youtube.com | 
				
			|||
localhost:3000 http://localhost:3000 | 
				
			|||
""".split '\n' | 
				
			|||
| 
		 Before Width: | Height: | Size: 460 B  | 
@ -1,115 +0,0 @@ | 
				
			|||
let p = console.log | 
				
			|||
import pkg from '../package.json' | 
				
			|||
let version = pkg.version | 
				
			|||
p "fuzzyhome version {version}" | 
				
			|||
 | 
				
			|||
# import sw from './sw.imba?serviceworker' | 
				
			|||
# navigator..serviceWorker..register(sw).then! do |reg| reg.update! | 
				
			|||
 | 
				
			|||
import { nanoid } from 'nanoid' | 
				
			|||
import { err } from './utils' | 
				
			|||
 | 
				
			|||
import db from './db' | 
				
			|||
import state from './state' | 
				
			|||
let refs = {} | 
				
			|||
import api from './api' | 
				
			|||
import config from './config' | 
				
			|||
 | 
				
			|||
import './components/app-home' | 
				
			|||
import './components/app-community-links' | 
				
			|||
import './components/app-settings' | 
				
			|||
import './components/app-prompt' | 
				
			|||
import './components/app-edit' | 
				
			|||
import './components/app-links' | 
				
			|||
import './components/app-link' | 
				
			|||
import './components/app-bang' | 
				
			|||
import './components/app-tips' | 
				
			|||
import './styles' | 
				
			|||
 | 
				
			|||
extend tag element | 
				
			|||
	get state | 
				
			|||
		state | 
				
			|||
	get api | 
				
			|||
		api | 
				
			|||
	get config | 
				
			|||
		config | 
				
			|||
	get refs | 
				
			|||
		refs | 
				
			|||
	get err | 
				
			|||
		err | 
				
			|||
	get version | 
				
			|||
		version | 
				
			|||
	get p | 
				
			|||
		console.log | 
				
			|||
 | 
				
			|||
tag app | 
				
			|||
 | 
				
			|||
	fatal_error = no | 
				
			|||
 | 
				
			|||
	get render? do mounted? | 
				
			|||
 | 
				
			|||
	def mount | 
				
			|||
 | 
				
			|||
		refs.settings = $as | 
				
			|||
		refs.edit = $ae | 
				
			|||
		refs.community-links = $acl | 
				
			|||
		refs.links = $ah | 
				
			|||
 | 
				
			|||
		unless global.localStorage.fuzzyhome_visited | 
				
			|||
			await api.add_initial_links! | 
				
			|||
			try | 
				
			|||
				let default_bang = await api.add_link '!google search https://www.google.com/search?q=' | 
				
			|||
				config.data.default_bang = default_bang | 
				
			|||
			global.localStorage.fuzzyhome_visited = yes | 
				
			|||
 | 
				
			|||
		try | 
				
			|||
			await api.reload_db! | 
				
			|||
			p "links:", state.links | 
				
			|||
		catch e | 
				
			|||
			err "state.loading database", e | 
				
			|||
			fatal_error = yes | 
				
			|||
			return | 
				
			|||
 | 
				
			|||
	def render | 
				
			|||
 | 
				
			|||
		<self | 
				
			|||
			.light=(config.theme is "light") | 
				
			|||
			.dark=(config.theme is "dark") | 
				
			|||
			.disabled=state.loading | 
				
			|||
		> | 
				
			|||
			css d:flex fld:column jc:start ai:center | 
				
			|||
				m:0 w:100% h:100% bg:$bodybg | 
				
			|||
				ff:sans-serif fw:1 | 
				
			|||
				user-select:none | 
				
			|||
 | 
				
			|||
			<.main> | 
				
			|||
				css d:flex fld:column jc:start ai:center | 
				
			|||
					bg:$appbg | 
				
			|||
					w:80vw max-width:700px max-height:80vh | 
				
			|||
					bxs:0px 0px 10px rgba(0,0,0,0.35) | 
				
			|||
					box-sizing:border-box p:30px rd:10px mt:10vh | 
				
			|||
 | 
				
			|||
				if fatal_error | 
				
			|||
					<.fatal> | 
				
			|||
						css c:$text-c | 
				
			|||
						""" | 
				
			|||
							There was an error state.loading the database. | 
				
			|||
							This could be due to a user setting | 
				
			|||
							disallowing local storage, or a random error. | 
				
			|||
							Consider refreshing. | 
				
			|||
							Check developer console for more information. | 
				
			|||
						""" | 
				
			|||
 | 
				
			|||
				elif $acl.active | 
				
			|||
					<app-community-links$acl> | 
				
			|||
 | 
				
			|||
				elif $as.active | 
				
			|||
					<app-settings$as> | 
				
			|||
 | 
				
			|||
				elif $ae.active | 
				
			|||
					<app-edit$ae> | 
				
			|||
 | 
				
			|||
				else | 
				
			|||
					<app-home$ah> | 
				
			|||
 | 
				
			|||
imba.mount <app> | 
				
			|||
@ -1,129 +0,0 @@ | 
				
			|||
tag app-bang | 
				
			|||
 | 
				
			|||
	def unmount | 
				
			|||
		state.bang_selection_index = -1 | 
				
			|||
 | 
				
			|||
	get tips | 
				
			|||
		let result = [] | 
				
			|||
		let temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: api.handle_bang.bind(api) | 
				
			|||
				hotkey_handler: api.handle_bang.bind(api) | 
				
			|||
				hotkey: 'return' | 
				
			|||
				hotkey_display_name: "Return" | 
				
			|||
		} | 
				
			|||
		if state.bang_selection_index > -1 | 
				
			|||
			temp.content = "Use History Item" | 
				
			|||
		else | 
				
			|||
			temp.content = "Search" | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
			click_handler: api.handle_add_link.bind(api) | 
				
			|||
			hotkey_handler: api.handle_add_link.bind(api) | 
				
			|||
			hotkey: 'shift+return' | 
				
			|||
			hotkey_display_name: 'Shift + Return' | 
				
			|||
			content: "Create Link \"{state.query.trim!}\"" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		if state.bang_selection_index > -1 | 
				
			|||
			temp = { | 
				
			|||
					click_handler: api.delete_bang_history_item.bind(api) | 
				
			|||
					hotkey_handler: api.delete_bang_history_item.bind(api) | 
				
			|||
					hotkey: 'shift+backspace' | 
				
			|||
					hotkey_display_name: "Shift + Backspace" | 
				
			|||
					content: "Delete History Item" | 
				
			|||
			} | 
				
			|||
			result.push temp | 
				
			|||
 | 
				
			|||
		if state.active_bang | 
				
			|||
			temp = { | 
				
			|||
					click_handler: api.unset_active_bang.bind(api) | 
				
			|||
					hotkey_handler: api.unset_active_bang.bind(api) | 
				
			|||
					hotkey: 'esc' | 
				
			|||
					hotkey_display_name: "Esc" | 
				
			|||
					content: "Back" | 
				
			|||
			} | 
				
			|||
			result.push temp | 
				
			|||
 | 
				
			|||
		def handle_delete_bang_history | 
				
			|||
			api.delete_bang_history! | 
				
			|||
			$tips.show_more = no | 
				
			|||
		temp = { | 
				
			|||
				click_handler: handle_delete_bang_history | 
				
			|||
				hotkey_display_name: "Click" | 
				
			|||
				content: "Delete Bang History" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: api.handle_cut.bind(api) | 
				
			|||
		} | 
				
			|||
		if api.math_result | 
				
			|||
			temp.hotkey_display_name = "Cut (If No Selection)" | 
				
			|||
			temp.content = "Cut All Text" | 
				
			|||
		else | 
				
			|||
			temp.hotkey_display_name = "Cut (Math, If No Selection)" | 
				
			|||
			temp.content = "Cut Math Result" | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				hotkey_display_name: "Paste (If Input Empty)" | 
				
			|||
				content: "Instant Search" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		result | 
				
			|||
 | 
				
			|||
	def render | 
				
			|||
 | 
				
			|||
		<self | 
				
			|||
			@hotkey("tab").force=api.increment_bang_selection_index | 
				
			|||
			@hotkey("up").force=api.decrement_bang_selection_index | 
				
			|||
			@hotkey("down").force=api.increment_bang_selection_index | 
				
			|||
			@hotkey("shift+tab").force=api.decrement_bang_selection_index | 
				
			|||
		> | 
				
			|||
			css w:100% d:flex fld:column gap:15px ofy:hidden | 
				
			|||
 | 
				
			|||
			<app-tips$tips tips=tips> | 
				
			|||
 | 
				
			|||
			unless $tips.show_more | 
				
			|||
 | 
				
			|||
				<.bang | 
				
			|||
					.selected=(state.bang_selection_index is -1) | 
				
			|||
					[c:$bang-c]=(state.bang_selection_index is -1) | 
				
			|||
					@pointerover=(state.bang_selection_index = -1) | 
				
			|||
					@click=api.handle_bang | 
				
			|||
				> | 
				
			|||
					css d:flex fld:row jc:space-between ai:center | 
				
			|||
						px:16px py:11px rd:5px cursor:pointer c:$text-c | 
				
			|||
 | 
				
			|||
					<.link-left> | 
				
			|||
						css d:flex fl:1 ofy:hidden | 
				
			|||
 | 
				
			|||
						<img.link-icon src=api.bang.icon> | 
				
			|||
							css w:20px h:20px mr:10px rd:3px | 
				
			|||
 | 
				
			|||
						<.display-name> "...{api.encoded_bang_query_nourl}" | 
				
			|||
							css fs:20px of:hidden text-overflow:ellipsis | 
				
			|||
 | 
				
			|||
					<.link-right> | 
				
			|||
						css d:flex fld:row jc:space-between ai:center | 
				
			|||
 | 
				
			|||
						<.frequency> api.bang.frequency | 
				
			|||
							css fs:15px ml:7px | 
				
			|||
 | 
				
			|||
				<.history> | 
				
			|||
					css d:flex fld:column jc:start ai:center ofy:auto | 
				
			|||
 | 
				
			|||
					for item, index in api.sorted_bang_history | 
				
			|||
						<.item | 
				
			|||
							@pointerover=(state.bang_selection_index = index) | 
				
			|||
							@click=api.handle_bang | 
				
			|||
							[c:$bang-c]=(state.bang_selection_index is index) | 
				
			|||
							.selected=(state.bang_selection_index is index) | 
				
			|||
						> item | 
				
			|||
							css w:100% fs:17px c:$text-c rd:5px p:10px 10px | 
				
			|||
								box-sizing:border-box cursor:pointer | 
				
			|||
@ -1,131 +0,0 @@ | 
				
			|||
let p = console.log | 
				
			|||
import all_links from '../assets/community_links' | 
				
			|||
import fzi from 'fzi' | 
				
			|||
import api from '../api' | 
				
			|||
 | 
				
			|||
tag app-community-links | 
				
			|||
 | 
				
			|||
	active = no | 
				
			|||
	selection_index = 0 | 
				
			|||
	query = "" | 
				
			|||
 | 
				
			|||
	get render? do mounted? | 
				
			|||
 | 
				
			|||
	def mount | 
				
			|||
		$cli.focus! | 
				
			|||
		links = await get_links! | 
				
			|||
		render! | 
				
			|||
 | 
				
			|||
	def get_links | 
				
			|||
		let result = [] | 
				
			|||
		for link_text in all_links | 
				
			|||
			let link = await api.create_link_from_text(link_text, no) | 
				
			|||
			link.link_text = link_text | 
				
			|||
			result.push link | 
				
			|||
		result | 
				
			|||
 | 
				
			|||
	get filtered_links | 
				
			|||
		links.filter! do |link| !api.name_exists(link.name) | 
				
			|||
 | 
				
			|||
	get sorted_links | 
				
			|||
		fzi.sort query, filtered_links, do |x| x.name | 
				
			|||
 | 
				
			|||
	get selected_link | 
				
			|||
		sorted_links[selection_index] | 
				
			|||
 | 
				
			|||
	def open | 
				
			|||
		active = yes | 
				
			|||
 | 
				
			|||
	def close | 
				
			|||
		active = no | 
				
			|||
 | 
				
			|||
	def increment_selection_index | 
				
			|||
		selection_index = Math.min(links.length - 1, selection_index + 1) | 
				
			|||
 | 
				
			|||
	def decrement_selection_index | 
				
			|||
		selection_index = Math.max(0, selection_index - 1) | 
				
			|||
 | 
				
			|||
	def add_community_link | 
				
			|||
		return if state.loading | 
				
			|||
		state.loading = yes | 
				
			|||
		try | 
				
			|||
			await api.add_link selected_link.link_text | 
				
			|||
		catch e | 
				
			|||
			err "adding link", e | 
				
			|||
		state.loading = no | 
				
			|||
 | 
				
			|||
	get tips | 
				
			|||
		let result = [] | 
				
			|||
		let temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: close.bind(this) | 
				
			|||
				hotkey_handler: close.bind(this) | 
				
			|||
				hotkey: "esc" | 
				
			|||
				hotkey_display_name: "Esc" | 
				
			|||
				content: "Exit Community Links" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: add_community_link.bind(this) | 
				
			|||
				hotkey_handler: add_community_link.bind(this) | 
				
			|||
				hotkey: "shift+return" | 
				
			|||
				hotkey_display_name: "Shift + Return Or Click" | 
				
			|||
				content: "Add To Your Links" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: increment_selection_index.bind(this) | 
				
			|||
				hotkey_handler: increment_selection_index.bind(this) | 
				
			|||
				hotkey: 'down' | 
				
			|||
				hotkey_display_name: "Down Arrow" | 
				
			|||
				content: "Move Selection Down" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: decrement_selection_index.bind(this) | 
				
			|||
				hotkey_handler: decrement_selection_index.bind(this) | 
				
			|||
				hotkey: 'up' | 
				
			|||
				hotkey_display_name: "Up Arrow" | 
				
			|||
				content: "Move Selection Up" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		result | 
				
			|||
 | 
				
			|||
	def render | 
				
			|||
 | 
				
			|||
		<self> | 
				
			|||
			css d:flex fld:column jc:start gap:15px fl:1 w:100% ofy:hidden | 
				
			|||
 | 
				
			|||
			<div> | 
				
			|||
				<input$cli | 
				
			|||
					autofocus | 
				
			|||
					bind=query | 
				
			|||
				> | 
				
			|||
 | 
				
			|||
			<app-tips tips=tips> | 
				
			|||
 | 
				
			|||
			<.links> | 
				
			|||
				css ofy:auto | 
				
			|||
 | 
				
			|||
				for link, index in sorted_links | 
				
			|||
					<.link | 
				
			|||
						.selected=(selection_index == index) | 
				
			|||
						@pointerover=(selection_index = index) | 
				
			|||
						@click=add_community_link | 
				
			|||
					> | 
				
			|||
						css d:flex fld:row jc:space-between ai:center px:16px | 
				
			|||
							py:2px rd:5px cursor:pointer c:$text-c min-height:35px | 
				
			|||
 | 
				
			|||
						if link.is_bang | 
				
			|||
							css c:$bang-c | 
				
			|||
 | 
				
			|||
						<.link-left> link.name | 
				
			|||
							css fl:1 | 
				
			|||
 | 
				
			|||
						<link-right.ellipsis> link.url | 
				
			|||
							css fl:1 c:inherit | 
				
			|||
@ -1,84 +0,0 @@ | 
				
			|||
tag app-edit | 
				
			|||
 | 
				
			|||
	active = no | 
				
			|||
 | 
				
			|||
	def mount | 
				
			|||
		$dn.setSelectionRange 0, 0 | 
				
			|||
		$dn.focus! | 
				
			|||
 | 
				
			|||
	def open data | 
				
			|||
		link = data | 
				
			|||
		new_link_text = value=api.construct_link_text(link) | 
				
			|||
		active = yes | 
				
			|||
 | 
				
			|||
	def close | 
				
			|||
		active = no | 
				
			|||
 | 
				
			|||
	def handle_click_set_default_bang | 
				
			|||
		config.set_default_bang link | 
				
			|||
		close! | 
				
			|||
 | 
				
			|||
	def handle_delete | 
				
			|||
		try | 
				
			|||
			await api.delete_link link | 
				
			|||
			close! | 
				
			|||
		catch e | 
				
			|||
			err "deleting link", e | 
				
			|||
 | 
				
			|||
	def save | 
				
			|||
		try | 
				
			|||
			api.update_link link, new_link_text | 
				
			|||
			close! | 
				
			|||
		catch e | 
				
			|||
			err "saving link", e | 
				
			|||
 | 
				
			|||
	get tips | 
				
			|||
		let result = [] | 
				
			|||
		let temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: handle_delete.bind(this) | 
				
			|||
				hotkey_handler: handle_delete.bind(this) | 
				
			|||
				hotkey: 'shift+backspace' | 
				
			|||
				hotkey_display_name: "Shift + Backspace" | 
				
			|||
				content: "Delete Link" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: save.bind(this) | 
				
			|||
				hotkey_handler: save.bind(this) | 
				
			|||
				hotkey: 'return' | 
				
			|||
				hotkey_display_name: "Return" | 
				
			|||
				content: "Update Link" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		if link.is_bang | 
				
			|||
			temp = { | 
				
			|||
					click_handler: handle_click_set_default_bang.bind(this) | 
				
			|||
					hotkey_display_name: "Click" | 
				
			|||
					content: "Set Default Bang" | 
				
			|||
			} | 
				
			|||
			result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: close.bind(this) | 
				
			|||
				hotkey_handler: close.bind(this) | 
				
			|||
				hotkey: 'esc' | 
				
			|||
				hotkey_display_name: "Esc" | 
				
			|||
				content: "Cancel" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		result | 
				
			|||
 | 
				
			|||
	def render | 
				
			|||
 | 
				
			|||
		<self> | 
				
			|||
			css d:flex fld:column gap:20px w:100% | 
				
			|||
 | 
				
			|||
			<div> | 
				
			|||
				<input$dn autofocus bind=new_link_text> | 
				
			|||
 | 
				
			|||
			<app-tips tips=tips> | 
				
			|||
@ -1,73 +0,0 @@ | 
				
			|||
tag app-home | 
				
			|||
 | 
				
			|||
	def mount | 
				
			|||
		$home-input.focus! | 
				
			|||
 | 
				
			|||
	def blur | 
				
			|||
		setTimeout(&, 100) do $home-input.focus! | 
				
			|||
 | 
				
			|||
	def handle_paste e | 
				
			|||
		return unless config.data.enable_search_on_paste | 
				
			|||
		return if state.query.length > 0 | 
				
			|||
		global.setTimeout(&, 0) do | 
				
			|||
			return if api.math_result isnt no | 
				
			|||
			bang ||= config.data.default_bang | 
				
			|||
			api.handle_bang! | 
				
			|||
 | 
				
			|||
	def handle_click_copy s | 
				
			|||
		try | 
				
			|||
			await window.navigator.clipboard.writeText(s) | 
				
			|||
			state.query = '' | 
				
			|||
			api.sort_links! | 
				
			|||
 | 
				
			|||
	def handle_input | 
				
			|||
		state.bang_selection_index = -1 | 
				
			|||
		api.set_link_selection_index 0 | 
				
			|||
		api.sort_links! | 
				
			|||
 | 
				
			|||
	def render | 
				
			|||
 | 
				
			|||
		<self> | 
				
			|||
			css w:100% d:flex fld:column ofy:hidden gap:20px | 
				
			|||
 | 
				
			|||
			<.header> | 
				
			|||
				css d:flex fld:row w:100% | 
				
			|||
				css .side c:$button-c fs:15px d:flex ja:center w:30px cursor:pointer | 
				
			|||
				css .side svg w:15px d:flex | 
				
			|||
				css .left jc:left | 
				
			|||
				css .right jc:right | 
				
			|||
 | 
				
			|||
				<.side.left@click=api.toggle_effective_names> | 
				
			|||
 | 
				
			|||
					if config.data.enable_effective_names | 
				
			|||
						<svg src="../assets/eye.svg"> | 
				
			|||
 | 
				
			|||
					else | 
				
			|||
						<svg src="../assets/eye-off.svg"> | 
				
			|||
 | 
				
			|||
				<input$home-input | 
				
			|||
					autofocus | 
				
			|||
					bind=state.query | 
				
			|||
					@input=handle_input | 
				
			|||
					@paste=handle_paste | 
				
			|||
					@cut=api.handle_cut | 
				
			|||
					disabled=state.loading | 
				
			|||
					@blur=blur | 
				
			|||
				> | 
				
			|||
					if state.query.startsWith "!" | 
				
			|||
						css c:$bang-c | 
				
			|||
 | 
				
			|||
				if (let m = api.math_result) isnt no | 
				
			|||
					<.side.right@click=handle_click_copy(m)> | 
				
			|||
						"= {Math.round(m * 100)/100}" | 
				
			|||
						css c:$text-c fs:20px ml:10px w:unset | 
				
			|||
 | 
				
			|||
				else | 
				
			|||
					<.side.right @click=refs.settings.open> | 
				
			|||
						<svg src="../assets/settings.svg"> | 
				
			|||
 | 
				
			|||
			if state.active_bang or state.sorted_links.length < 1 | 
				
			|||
				<app-bang> | 
				
			|||
 | 
				
			|||
			else | 
				
			|||
				<app-links> | 
				
			|||
@ -1,67 +0,0 @@ | 
				
			|||
tag app-link | 
				
			|||
 | 
				
			|||
	def handle_delete link | 
				
			|||
		return unless window.confirm "Do you really want to delete {link..display_name}?" | 
				
			|||
		api.delete_link link | 
				
			|||
 | 
				
			|||
	def handle_pin link | 
				
			|||
		api.pin_link link | 
				
			|||
 | 
				
			|||
	def render | 
				
			|||
		<self | 
				
			|||
			@pointerover=api.set_link_selection_index(index) | 
				
			|||
			@click=api.handle_click_link | 
				
			|||
			.selected=(index is state.link_selection_index) | 
				
			|||
		> | 
				
			|||
			css d:flex fld:row jc:space-between ai:center | 
				
			|||
				px:16px py:11px rd:5px cursor:pointer c:$text-c | 
				
			|||
			if link.is_bang | 
				
			|||
				css c:$bang-c | 
				
			|||
 | 
				
			|||
			<.link-left> | 
				
			|||
				css d:flex fl:3 | 
				
			|||
 | 
				
			|||
				<img.link-icon src=link.icon> | 
				
			|||
					css w:20px h:20px mr:10px rd:3px | 
				
			|||
 | 
				
			|||
				<.display-name> link.display_name | 
				
			|||
					css tt:capitalize fs:20px overflow-wrap:anywhere | 
				
			|||
 | 
				
			|||
				if link.display_name isnt link.name and config.data.enable_effective_names | 
				
			|||
					<.name> | 
				
			|||
						css d:flex ja:center c:$effective-name-c ml:10px fs:14px | 
				
			|||
						css .parens fs:10px c:$effective-name-parens-c | 
				
			|||
 | 
				
			|||
						<span.parens> "(" | 
				
			|||
						<span> link.name | 
				
			|||
						<span.parens> ")" | 
				
			|||
 | 
				
			|||
			<.link-right> | 
				
			|||
				css fl:1 d:flex fld:row jc:space-between ai:center | 
				
			|||
 | 
				
			|||
				css .buttons-disabled .link-button visibility:hidden | 
				
			|||
				css .selected .link-button visibility:visible | 
				
			|||
 | 
				
			|||
				<.link-buttons .buttons-disabled=!config.data.enable_buttons> | 
				
			|||
					css d:flex fld:row jc:start ai:center gap:5px | 
				
			|||
 | 
				
			|||
					css .link-button visibility:hidden rd:3px c:$button-c fs:15px cursor:pointer px:3px | 
				
			|||
					if index is state.link_selection_index | 
				
			|||
						css .link-button visibility:visible | 
				
			|||
 | 
				
			|||
					css .link-button svg w:15px | 
				
			|||
 | 
				
			|||
					<.link-button@click.prevent.stop=handle_edit> | 
				
			|||
						<svg src='../assets/edit-2.svg'> | 
				
			|||
 | 
				
			|||
					<.link-button@click.prevent.stop=handle_delete(link)> | 
				
			|||
						<svg src='../assets/trash.svg'> | 
				
			|||
 | 
				
			|||
					<.link-button @click.prevent.stop=handle_pin(link)> | 
				
			|||
						if link.is_pinned | 
				
			|||
							css visibility:visible c:$button-dim-c | 
				
			|||
 | 
				
			|||
						<svg src='../assets/star.svg'> | 
				
			|||
 | 
				
			|||
				<.frequency> link.frequency | 
				
			|||
					css fs:15px ml:7px | 
				
			|||
@ -1,104 +0,0 @@ | 
				
			|||
tag app-links | 
				
			|||
 | 
				
			|||
	def handle_edit | 
				
			|||
		return unless state.sorted_links.length > 0 | 
				
			|||
		refs.edit.open api.selected_link | 
				
			|||
 | 
				
			|||
	get tips | 
				
			|||
		let result = [] | 
				
			|||
		let temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
			click_handler: api.handle_click_link.bind(api) | 
				
			|||
			hotkey_handler: api.handle_click_link.bind(api) | 
				
			|||
			hotkey: 'return' | 
				
			|||
			hotkey_display_name: 'Return' | 
				
			|||
		} | 
				
			|||
		temp.content = api.selected_link.is_bang ? "Use Bang" : "Navigate To Link" | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
			click_handler: api.handle_add_link.bind(api) | 
				
			|||
			hotkey_handler: api.handle_add_link.bind(api) | 
				
			|||
			hotkey: 'shift+return' | 
				
			|||
			hotkey_display_name: 'Shift + Return' | 
				
			|||
			content: "Create Link \"{state.query.trim!}\"" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: handle_edit.bind(this) | 
				
			|||
				hotkey_handler: handle_edit.bind(this) | 
				
			|||
				hotkey: 'shift+backspace' | 
				
			|||
				hotkey_display_name: "Shift + Backspace" | 
				
			|||
				content: "Edit Link" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: api.toggle_effective_names.bind(api) | 
				
			|||
				hotkey_handler: api.toggle_effective_names.bind(api) | 
				
			|||
				hotkey: 'tab' | 
				
			|||
				hotkey_display_name: "Tab" | 
				
			|||
				content: "Toggle Effective Names" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: (do refs.settings.open!).bind(this) | 
				
			|||
				hotkey_handler: (do refs.settings.open!).bind(this) | 
				
			|||
				hotkey: 'shift+tab' | 
				
			|||
				hotkey_display_name: "Shift + Tab" | 
				
			|||
				content: "Toggle Settings" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: api.handle_cut.bind(api) | 
				
			|||
		} | 
				
			|||
		if api.math_result | 
				
			|||
			temp.hotkey_display_name = "Cut (If No Selection)" | 
				
			|||
			temp.content = "Cut All Text" | 
				
			|||
		else | 
				
			|||
			temp.hotkey_display_name = "Cut (Math, If No Selection)" | 
				
			|||
			temp.content = "Cut Math Result" | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: api.increment_link_selection_index.bind(api) | 
				
			|||
				hotkey_handler: api.increment_link_selection_index.bind(api) | 
				
			|||
				hotkey: 'down' | 
				
			|||
				hotkey_display_name: "Down Arrow" | 
				
			|||
				content: "Move Selection Down" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: api.decrement_link_selection_index.bind(api) | 
				
			|||
				hotkey_handler: api.decrement_link_selection_index.bind(api) | 
				
			|||
				hotkey: 'up' | 
				
			|||
				hotkey_display_name: "Up Arrow" | 
				
			|||
				content: "Move Selection Up" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				hotkey_display_name: "Paste (If Input Empty)" | 
				
			|||
				content: "Instant Search" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		result | 
				
			|||
 | 
				
			|||
	def render | 
				
			|||
 | 
				
			|||
		<self> | 
				
			|||
			css w:100% d:flex fld:column gap:15px ofy:hidden | 
				
			|||
 | 
				
			|||
			<app-tips$tips tips=tips> | 
				
			|||
 | 
				
			|||
			<.links> | 
				
			|||
				css ofy:auto | 
				
			|||
				for link, index in state.sorted_links | 
				
			|||
					<app-link link=link index=index handle_edit=handle_edit> | 
				
			|||
 | 
				
			|||
@ -1,16 +0,0 @@ | 
				
			|||
import { cloneDeep } from 'lodash' | 
				
			|||
 | 
				
			|||
tag app-prompt | 
				
			|||
 | 
				
			|||
	def get_input d | 
				
			|||
		data = cloneDeep d | 
				
			|||
		active = yes | 
				
			|||
		let result = await new Promise! do |resolve| | 
				
			|||
			self.addEventListener('end') do |e| | 
				
			|||
				self.removeEventListener('end', this) | 
				
			|||
				resolve(e.detail) | 
				
			|||
		active = no | 
				
			|||
		result | 
				
			|||
 | 
				
			|||
	def end | 
				
			|||
		emit('end', data) | 
				
			|||
@ -1,153 +0,0 @@ | 
				
			|||
import download from 'downloadjs' | 
				
			|||
 | 
				
			|||
tag app-settings | 
				
			|||
 | 
				
			|||
	active = no | 
				
			|||
 | 
				
			|||
	def close | 
				
			|||
		active = no | 
				
			|||
 | 
				
			|||
	def open | 
				
			|||
		active = yes | 
				
			|||
 | 
				
			|||
	def handle_click_github | 
				
			|||
		global.location.href = "https://github.com/familyfriendlymikey/fuzzyhome" | 
				
			|||
 | 
				
			|||
	def handle_click_toggle_tips | 
				
			|||
		config.data.enable_tips = not config.data.enable_tips | 
				
			|||
		config.save! | 
				
			|||
		active = no | 
				
			|||
 | 
				
			|||
	def handle_click_toggle_buttons | 
				
			|||
		config.data.enable_buttons = not config.data.enable_buttons | 
				
			|||
		config.save! | 
				
			|||
		active = no | 
				
			|||
 | 
				
			|||
	def handle_click_toggle_search_on_paste | 
				
			|||
		config.data.enable_search_on_paste = not config.data.enable_search_on_paste | 
				
			|||
		config.save! | 
				
			|||
		active = no | 
				
			|||
 | 
				
			|||
	def handle_toggle_light_theme | 
				
			|||
		config.data.enable_light_theme = not config.data.enable_light_theme | 
				
			|||
		config.save! | 
				
			|||
		active = no | 
				
			|||
 | 
				
			|||
	def handle_import e | 
				
			|||
 | 
				
			|||
		def handle_import | 
				
			|||
			let errors = [] | 
				
			|||
			try | 
				
			|||
				let text = await e.target.files[0].text! | 
				
			|||
				var links = text.split "\n" | 
				
			|||
			catch e | 
				
			|||
				return err "importing db", e | 
				
			|||
			for link_text in links | 
				
			|||
				try | 
				
			|||
					let link = await api.create_link_from_text link_text | 
				
			|||
					if api.name_exists link.name | 
				
			|||
						throw "Name already exists, add manually if you don't mind duplicates." | 
				
			|||
					api.add_link link_text | 
				
			|||
				catch e | 
				
			|||
					errors.push "{link_text}\n{e}" | 
				
			|||
			if errors.length > 0 | 
				
			|||
				err "importing some links", errors.join("\n\n") | 
				
			|||
 | 
				
			|||
		state.loading = yes | 
				
			|||
		await handle_import! | 
				
			|||
		active = no | 
				
			|||
		state.loading = no | 
				
			|||
		close! | 
				
			|||
 | 
				
			|||
	def handle_click_export | 
				
			|||
		state.loading = yes | 
				
			|||
		await api.reload_db! | 
				
			|||
		let links = state.links.map do |link| | 
				
			|||
			api.construct_link_text link | 
				
			|||
		let datetime = new Date!.toString!.split(" ") | 
				
			|||
		let date = datetime.slice(1, 4).join("-").toLowerCase! | 
				
			|||
		let time = datetime[4].split(":").join("-") | 
				
			|||
		let filename = "fuzzyhome_v{version}_{date}_{time}.txt" | 
				
			|||
		download(links.join("\n"), filename, "text/plain") | 
				
			|||
		active = no | 
				
			|||
		state.loading = no | 
				
			|||
 | 
				
			|||
	def render | 
				
			|||
 | 
				
			|||
		<self> | 
				
			|||
			css w:100% | 
				
			|||
 | 
				
			|||
			css .settings-container | 
				
			|||
				d:flex fld:row jc:space-around ai:center | 
				
			|||
				w:100% h:50px | 
				
			|||
				mt:10px | 
				
			|||
				gap:10px | 
				
			|||
 | 
				
			|||
			css .settings-button, .settings-container button | 
				
			|||
				d:flex fld:column jc:center ai:center fl:1 | 
				
			|||
				bg:none bd:none cursor:pointer fs:14px | 
				
			|||
				rd:5px | 
				
			|||
				transition:background 100ms | 
				
			|||
				h:100% | 
				
			|||
				bg:$button-bg c:$button-c | 
				
			|||
				@hover bg:$button-hover-bg | 
				
			|||
 | 
				
			|||
			if refs.community-links.active | 
				
			|||
				<app-community-links> | 
				
			|||
 | 
				
			|||
			else | 
				
			|||
				<.settings-container> | 
				
			|||
 | 
				
			|||
					<.settings-button | 
				
			|||
						@click=close | 
				
			|||
						@hotkey("esc")=close | 
				
			|||
						@hotkey("shift+tab")=close | 
				
			|||
					> "BACK" | 
				
			|||
 | 
				
			|||
				<.settings-container> | 
				
			|||
 | 
				
			|||
					<.settings-button @click=(refs.community-links.open! and close!)> | 
				
			|||
						"VIEW COMMUNITY LINKS" | 
				
			|||
 | 
				
			|||
				<.settings-container> | 
				
			|||
 | 
				
			|||
					<label.settings-button> | 
				
			|||
						"IMPORT" | 
				
			|||
						<input[d:none] | 
				
			|||
							disabled=state.loading | 
				
			|||
							@change=handle_import | 
				
			|||
							@click=(this.value = '') | 
				
			|||
							type="file" | 
				
			|||
						> | 
				
			|||
 | 
				
			|||
					<.settings-button @click=handle_click_export> | 
				
			|||
						"EXPORT" | 
				
			|||
 | 
				
			|||
				<.settings-container> | 
				
			|||
 | 
				
			|||
					<.settings-button @click=handle_click_github> | 
				
			|||
						"TUTORIAL" | 
				
			|||
 | 
				
			|||
					<.settings-button @click=handle_click_github> | 
				
			|||
						"GITHUB" | 
				
			|||
 | 
				
			|||
				<.settings-container> | 
				
			|||
 | 
				
			|||
					<.settings-button @click=handle_click_toggle_tips> | 
				
			|||
						config.data.enable_tips ? "DISABLE TIPS" : "ENABLE TIPS" | 
				
			|||
 | 
				
			|||
					<.settings-button @click=handle_click_toggle_buttons> | 
				
			|||
						config.data.enable_buttons ? "DISABLE BUTTONS" : "ENABLE BUTTONS" | 
				
			|||
 | 
				
			|||
				<.settings-container> | 
				
			|||
 | 
				
			|||
					<.settings-button @click=handle_click_toggle_search_on_paste> | 
				
			|||
						config.data.enable_search_on_paste ? "DISABLE SEARCH ON PASTE" : "ENABLE SEARCH ON PASTE" | 
				
			|||
 | 
				
			|||
					<.settings-button @click=config.cycle_theme> | 
				
			|||
						"THEME: {config.data.theme.toUpperCase!}" | 
				
			|||
 | 
				
			|||
				<.settings-container> | 
				
			|||
 | 
				
			|||
					<.settings-button @click=(api.delete_all_bang_history! and close!)> | 
				
			|||
						"DELETE ALL BANG HISTORY" | 
				
			|||
@ -1,86 +0,0 @@ | 
				
			|||
import { chunk, fill } from 'lodash' | 
				
			|||
 | 
				
			|||
tag app-tip | 
				
			|||
 | 
				
			|||
	<self | 
				
			|||
		@click.if(tip.click_handler)=tip.click_handler | 
				
			|||
	> | 
				
			|||
		css d:flex fld:column jc:start fl:1 | 
				
			|||
			bdr:1px solid | 
				
			|||
			bc:$tip-bc | 
				
			|||
			min-width:0 ta:center p:10px | 
				
			|||
			cursor:pointer transition:background 100ms | 
				
			|||
			@first ta:left rdl:3px | 
				
			|||
			@last ta:right bd:none rdr:3px | 
				
			|||
			@hover bg:$tip-hover-c | 
				
			|||
		if tip.placeholder or not tip.click_handler | 
				
			|||
			css | 
				
			|||
				@hover @important cursor:auto bg:none | 
				
			|||
 | 
				
			|||
		if tip.hotkey_handler and tip.hotkey | 
				
			|||
			<@hotkey(tip.hotkey).force=tip.hotkey_handler> | 
				
			|||
				css d:none | 
				
			|||
 | 
				
			|||
		<.tip-hotkey> tip.hotkey_display_name | 
				
			|||
			css fs:12px c:$tip-hotkey-c | 
				
			|||
 | 
				
			|||
		<.tip-content> tip.content | 
				
			|||
			css pt:2px fs:14px c:$tip-content-c | 
				
			|||
 | 
				
			|||
tag app-tips | 
				
			|||
 | 
				
			|||
	def unmount | 
				
			|||
		show_more = no | 
				
			|||
 | 
				
			|||
	def toggle | 
				
			|||
		show_more = not show_more | 
				
			|||
 | 
				
			|||
	def pad arr | 
				
			|||
		let i = arr.length | 
				
			|||
		while i < 3 | 
				
			|||
			arr.push { placeholder: yes } | 
				
			|||
			i += 1 | 
				
			|||
 | 
				
			|||
	def get_chunks | 
				
			|||
		let chunks = chunk(tips, 3) | 
				
			|||
		pad(chunks[-1]) | 
				
			|||
		chunks | 
				
			|||
 | 
				
			|||
	def render | 
				
			|||
		let chunks = get_chunks! | 
				
			|||
 | 
				
			|||
		<self[d:none]=!config.data.enable_tips> | 
				
			|||
			css d:flex fld:column gap:15px max-height:75% | 
				
			|||
 | 
				
			|||
			css .tip-row | 
				
			|||
				d:flex fld:row w:100% fl:1 | 
				
			|||
				fs:20px fs:14px | 
				
			|||
				jc:end ta:center | 
				
			|||
 | 
				
			|||
			<.tip-row> | 
				
			|||
				for tip in chunks[0] | 
				
			|||
					<app-tip tip=tip> | 
				
			|||
 | 
				
			|||
			if chunks.length > 1 | 
				
			|||
 | 
				
			|||
				<@click=toggle> | 
				
			|||
					css w:100% d:flex ja:center c:$button-c rdb:4px cursor:pointer | 
				
			|||
						transition:background 100ms | 
				
			|||
						@hover bg:$tip-hover-c | 
				
			|||
					if show_more | 
				
			|||
						css rd:0 | 
				
			|||
 | 
				
			|||
					<svg src="../assets/chevron-down.svg"> | 
				
			|||
						css w:15px transition:transform 150ms | 
				
			|||
						if show_more | 
				
			|||
							css transform:rotate(180deg) | 
				
			|||
 | 
				
			|||
				<.more> | 
				
			|||
					css d:flex fld:column gap:15px ofy:auto  | 
				
			|||
					unless show_more | 
				
			|||
						css d:none | 
				
			|||
 | 
				
			|||
					for row in chunks.slice(1) | 
				
			|||
						<.tip-row> | 
				
			|||
							for tip in row | 
				
			|||
								<app-tip tip=tip> | 
				
			|||
@ -1,51 +0,0 @@ | 
				
			|||
let p = console.log | 
				
			|||
import Dexie from 'dexie' | 
				
			|||
import 'dexie-export-import' | 
				
			|||
import { nanoid } from 'nanoid' | 
				
			|||
import api from './api' | 
				
			|||
 | 
				
			|||
let db = new Dexie 'fuzzyhome' | 
				
			|||
 | 
				
			|||
db.version(1).stores({ | 
				
			|||
	links: "++id,name,link" | 
				
			|||
}) | 
				
			|||
 | 
				
			|||
db.version(2).stores({ | 
				
			|||
	links: "++id,name,url,frequency,img" | 
				
			|||
}).upgrade! do |trans| | 
				
			|||
	p "upgrading to fuzzyhome db version 2" | 
				
			|||
	trans.links.toCollection!.modify! do |link| | 
				
			|||
		let id = nanoid! | 
				
			|||
		let name = link.name | 
				
			|||
		let url = link.link | 
				
			|||
		let frequency = link.frequency | 
				
			|||
		let img = link.img | 
				
			|||
		this.value = { id, name, url, frequency, img } | 
				
			|||
 | 
				
			|||
db.version(3).stores({ | 
				
			|||
	links: "++id,name,url,frequency,img" | 
				
			|||
}).upgrade! do |trans| | 
				
			|||
	p "upgrading to fuzzyhome db version 3" | 
				
			|||
	trans.links.toCollection!.modify! do |link| | 
				
			|||
		try | 
				
			|||
			link.url = api.parse_url(link.url).href | 
				
			|||
 | 
				
			|||
db.version(4).stores({ | 
				
			|||
	links: "++id,display_name,name,is_bang,url,frequency,icon" | 
				
			|||
}).upgrade! do |trans| | 
				
			|||
	p "upgrading to fuzzyhome db version 4" | 
				
			|||
	trans.links.toCollection!.modify! do |link| | 
				
			|||
		link.display_name = link.name | 
				
			|||
		link.is_bang = no | 
				
			|||
		link.icon = link.img | 
				
			|||
		delete link.img | 
				
			|||
 | 
				
			|||
db.version(5).stores({ | 
				
			|||
	links: "++id,display_name,name,is_bang,is_pinned,url,frequency,history,icon" | 
				
			|||
}).upgrade! do |trans| | 
				
			|||
	p "upgrading to fuzzyhome db version 5" | 
				
			|||
	trans.links.toCollection!.modify! do |link| | 
				
			|||
		link.is_pinned = no | 
				
			|||
		link.history = [] | 
				
			|||
 | 
				
			|||
export default db | 
				
			|||
@ -1,41 +0,0 @@ | 
				
			|||
const app_name = "fuzzyhome" | 
				
			|||
import { version } from '../package.json' | 
				
			|||
const app_prefix = "{app_name}_cache" | 
				
			|||
const cache_name = "sw-{app_prefix}-{version}" | 
				
			|||
let p = do |s| console.log "{cache_name} {s}" | 
				
			|||
p "loaded" | 
				
			|||
 | 
				
			|||
let urls = [ | 
				
			|||
	'./' | 
				
			|||
] | 
				
			|||
 | 
				
			|||
self.addEventListener('fetch') do |e| | 
				
			|||
	p "fetch" | 
				
			|||
	def intercept request | 
				
			|||
		if request | 
				
			|||
			p "responding with cache {e.request.url}" | 
				
			|||
			request | 
				
			|||
		else | 
				
			|||
			p "not cached, fetching {e.request.url}" | 
				
			|||
			fetch e.request | 
				
			|||
	e.respondWith(caches.match(e.request.url).then(intercept)) | 
				
			|||
 | 
				
			|||
self.addEventListener('install') do |e| | 
				
			|||
	p "install" | 
				
			|||
	def add_urls_to_cache cache | 
				
			|||
		p "adding urls to cache" | 
				
			|||
		cache.addAll urls | 
				
			|||
		skipWaiting! | 
				
			|||
	e.waitUntil(caches.open(cache_name).then(add_urls_to_cache)) | 
				
			|||
 | 
				
			|||
self.addEventListener('activate') do |e| | 
				
			|||
	p "activate" | 
				
			|||
	def delete_cached keys | 
				
			|||
		let temp = keys.map! do |key, i| | 
				
			|||
			p "checking cache {key}" | 
				
			|||
			if key !== cache_name | 
				
			|||
				p "deleting cache {key}" | 
				
			|||
				let result = await caches.delete key | 
				
			|||
				p "deletion of {key} result: {result}" | 
				
			|||
		Promise.all(temp) | 
				
			|||
	e.waitUntil(caches.keys().then(delete_cached)) | 
				
			|||
@ -1,13 +0,0 @@ | 
				
			|||
let p = console.log | 
				
			|||
 | 
				
			|||
export def err s, e | 
				
			|||
	p "error:" | 
				
			|||
	p e | 
				
			|||
	window.alert("Error {s}:\n\n{e}") | 
				
			|||
 | 
				
			|||
global._fuzzyhome_delete_everything = do |prompt=yes| | 
				
			|||
	return if prompt and window.confirm "This will delete everything. Are you sure?" | 
				
			|||
	indexedDB.deleteDatabase("fuzzyhome") | 
				
			|||
	delete localStorage.fuzzyhome_config | 
				
			|||
	delete localStorage.fuzzyhome_visited | 
				
			|||
	location.reload! | 
				
			|||
@ -0,0 +1,13 @@ | 
				
			|||
{ | 
				
			|||
	"name": "fuzzyhome", | 
				
			|||
	"description": "A power user oriented new-tab page that enables lightning speed navigation through the dark magic of fuzzy finding.", | 
				
			|||
	"version": "2.1.0", | 
				
			|||
	"manifest_version": 3, | 
				
			|||
	"permissions": [ | 
				
			|||
		"bookmarks", | 
				
			|||
		"storage" | 
				
			|||
	], | 
				
			|||
	"chrome_url_overrides": { | 
				
			|||
		"newtab": "dist/index.html" | 
				
			|||
	} | 
				
			|||
} | 
				
			|||
@ -1,26 +1,19 @@ | 
				
			|||
{ | 
				
			|||
	"name": "fuzzyhome", | 
				
			|||
	"version": "0.1.43", | 
				
			|||
	"version": "0.2.1", | 
				
			|||
	"description": "A lightweight new-tab page that lets you very quickly fuzzy find links and navigate to a result.", | 
				
			|||
	"homepage": "https://fuzzyho.me/", | 
				
			|||
	"author": "Mikey Oz (https://github.com/familyfriendlymikey)", | 
				
			|||
	"scripts": { | 
				
			|||
		"build": "imba build app/index.html", | 
				
			|||
		"publish-preview": "npx gh-pages --no-history --dotfiles --dist dist/ --branch preview", | 
				
			|||
		"publish-release": "npx gh-pages --no-history --dotfiles --dist dist/ --branch release", | 
				
			|||
		"start": "imba -w app/index.html", | 
				
			|||
		"static": "npm run build && cd dist && python3 -m http.server", | 
				
			|||
		"preview": "npm run build && npm run publish-preview", | 
				
			|||
		"deploy-only-after-preview": "npm run publish-release" | 
				
			|||
		"dev": "vite build -w --outDir chrome/dist --minify false", | 
				
			|||
		"build": "vite build --outDir chrome/dist" | 
				
			|||
	}, | 
				
			|||
	"dependencies": { | 
				
			|||
		"dexie": "^3.2.2", | 
				
			|||
		"dexie-export-import": "^1.0.3", | 
				
			|||
		"downloadjs": "^1.4.7", | 
				
			|||
		"fzi": "^1.1.0", | 
				
			|||
		"imba": "^2.0.0-alpha.220", | 
				
			|||
		"fzi": "^1.5.0", | 
				
			|||
		"imba": "^2.0.0-alpha.225", | 
				
			|||
		"lodash": "^4.17.21", | 
				
			|||
		"mathjs": "^11.1.0", | 
				
			|||
		"nanoid": "^4.0.0" | 
				
			|||
	}, | 
				
			|||
	"description": "A lightweight new-tab page that lets you very quickly fuzzy find links and navigate to a result.", | 
				
			|||
	"homepage": "https://fuzzyho.me/", | 
				
			|||
	"author": "Mikey Oz (https://github.com/familyfriendlymikey)" | 
				
			|||
		"math-expression-evaluator": "^1.4.0", | 
				
			|||
		"vite": "^3.2.5", | 
				
			|||
		"vite-plugin-imba": "^0.10.1" | 
				
			|||
	} | 
				
			|||
} | 
				
			|||
 | 
				
			|||
@ -1,158 +1,14 @@ | 
				
			|||
<h1 align="center"> | 
				
			|||
<a href="https://fuzzyho.me/"> | 
				
			|||
fuzzyhome | 
				
			|||
</a> | 
				
			|||
</h1> | 
				
			|||
useful modifications: | 
				
			|||
 | 
				
			|||
A power user oriented new-tab page that enables lightning speed navigation through the dark magic of fuzzy finding. | 
				
			|||
cmd+option+b for bookmark manager | 
				
			|||
cmd+h for home instead of closing tab and opening new tab | 
				
			|||
 | 
				
			|||
## Installation | 
				
			|||
1. Copy this link: `https://fuzzyho.me/`: | 
				
			|||
1. Change your homepage in your browser settings. | 
				
			|||
1. Install a browser extension that lets you change your new-tab page url. These have worked fine for me: | 
				
			|||
	- Firefox: [New Tab Override](https://addons.mozilla.org/en-US/firefox/addon/new-tab-override) | 
				
			|||
	- Chrome: [New Tab Redirect](https://chrome.google.com/webstore/detail/new-tab-redirect/icpgjfneehieebagbmdbhnlpiopdcmna) | 
				
			|||
This is the latest working preview of fuzzyhome which can be | 
				
			|||
found at https://preview.fuzzyhome.pages.dev/ | 
				
			|||
 | 
				
			|||
	If the extension has an option for setting focus to the webpage instead of the address bar, be sure to enable it. | 
				
			|||
 | 
				
			|||
## Usage | 
				
			|||
 | 
				
			|||
### Create | 
				
			|||
Create a new link by typing a name and a url separated by a space. | 
				
			|||
For example: | 
				
			|||
``` | 
				
			|||
imba home page https://imba.io/ | 
				
			|||
``` | 
				
			|||
The last space-separated string will be used as the url for your link. | 
				
			|||
In most cases if you do not specify a protocol, `https` will be used. | 
				
			|||
 | 
				
			|||
You can also use the hotkey `shift+return` to create a new link. | 
				
			|||
 | 
				
			|||
### Fuzzy Find | 
				
			|||
Search for a link by typing. | 
				
			|||
The fuzzy sorting algorithm makes searching very fast, | 
				
			|||
as you can usually just type the first letter of each word to get to a link (`ihp` to get to `imba home page`, for example). | 
				
			|||
 | 
				
			|||
### Navigate | 
				
			|||
Navigate to the currently selected search result by pressing `return`. | 
				
			|||
You can also click on a link to navigate to it. | 
				
			|||
You can also press the up or down arrow keys to move your selection up and down. | 
				
			|||
 | 
				
			|||
### Search | 
				
			|||
If there are no matching links, a search will be performed with your query. | 
				
			|||
 | 
				
			|||
### Bangs | 
				
			|||
There may be some websites you've created links for, such as amazon, where you almost always search for something. | 
				
			|||
This means you have to go to that website, click the search bar, and type in your query. | 
				
			|||
With fuzzyhome you can cut out the slow parts by prefixing your link name with `!` to create a "bank link": | 
				
			|||
``` | 
				
			|||
!amazon amazon.com/s?k= | 
				
			|||
``` | 
				
			|||
Notice the `/s?k=` at the end, | 
				
			|||
you'll likewise have to find the proper URL for your new bang link. | 
				
			|||
Typing instructions for that would be too verbose, so please see the video tutorial (coming soon). | 
				
			|||
 | 
				
			|||
When you click on or press return on a selected bang link, | 
				
			|||
instead of navigating directly to that link, | 
				
			|||
you'll be able to enter a search query for that link. | 
				
			|||
Pressing enter again will bring you to the link with your encoded search query appended to it. | 
				
			|||
 | 
				
			|||
**LIST OF BANGS FOR YOUR CONVENIENCE BELOW** | 
				
			|||
 | 
				
			|||
### Effective Names | 
				
			|||
After using fuzzyhome enough, you may come to realize | 
				
			|||
that there are some links you'd prefer be "hardcoded" | 
				
			|||
with certain names so to speak. | 
				
			|||
For example, perhaps you visit `instagram` extremely often | 
				
			|||
but also have a link named `indeed` which gets sorted above | 
				
			|||
`instagram` when you type `in` even though you visit it much less often. | 
				
			|||
You could change the `instagram` link's name to `in`, | 
				
			|||
but now it looks bad. | 
				
			|||
To solve this, fuzzyhome allows you to add an "effective name" | 
				
			|||
to a link: | 
				
			|||
``` | 
				
			|||
instagram `in instagram.com | 
				
			|||
``` | 
				
			|||
To add an effective name to a link, | 
				
			|||
simply add the name prefixed with a backtick right before the URL. | 
				
			|||
 | 
				
			|||
This also works for bang links. | 
				
			|||
Let's say we wanted `a` to correspond to `!amazon`: | 
				
			|||
``` | 
				
			|||
!amazon `a amazon.com/s?k= | 
				
			|||
``` | 
				
			|||
Now when you type `in` or `a`, | 
				
			|||
you can have confidence that your | 
				
			|||
intended link will be given priority every time. | 
				
			|||
 | 
				
			|||
Mind you, typing `am` will no longer show `amazon` in the results, | 
				
			|||
because that's just the display name for the link. | 
				
			|||
The actual name is `a`. | 
				
			|||
This might seem confusing but once you | 
				
			|||
get the hang of it won't matter to you at all. | 
				
			|||
 | 
				
			|||
### Delete | 
				
			|||
You can delete notes by clicking the purple `x` on the currently selected link. | 
				
			|||
You can also use the hotkey `shift+backspace` to delete the currently selected link. | 
				
			|||
 | 
				
			|||
### Edit | 
				
			|||
You can edit notes by clicking the edit icon. | 
				
			|||
 | 
				
			|||
### Move Selection | 
				
			|||
You can move your selection up and down with the arrow keys. | 
				
			|||
 | 
				
			|||
### Quick Search | 
				
			|||
If you paste while the input is empty, fuzzyhome will immediately make a search with your pasted query. | 
				
			|||
 | 
				
			|||
### Customize Search Engine | 
				
			|||
The default search engine is Google Search, however you can customize it by clicking the three dots to go to settings, | 
				
			|||
clicking `config`, and pasting in your search engine url, such as `https://search.brave.com/search?q=`. | 
				
			|||
Your search query simply gets encoded and pasted to the end of your configured search engine url. | 
				
			|||
 | 
				
			|||
### Importing / Exporting Links | 
				
			|||
If you want to export your links to use them on another computer, go to the settings menu and click `EXPORT`. | 
				
			|||
This downloads a `.json` file, which you can then send to your other computer and import by clicking the `IMPORT` button | 
				
			|||
and selecting your file. | 
				
			|||
 | 
				
			|||
### Reset Everything To Default | 
				
			|||
Not sure why anyone except me would do this, but if for some reason you want to delete everything and restore the default config, | 
				
			|||
you can do so by bringing up your developer console and running the function `_fuzzyhome_delete_everything()`, | 
				
			|||
and confirming that you do indeed want to delete all your links on the prompt that pops up. | 
				
			|||
 | 
				
			|||
## Hotkeys | 
				
			|||
Hotkey | Action | 
				
			|||
-|- | 
				
			|||
Return | Navigate to the currently selected link, or perform a search if there are no matching links. | 
				
			|||
Up Arrow | Move selection up. | 
				
			|||
Down Arrow | Move selection down. | 
				
			|||
Paste | If input is empty, immediately search with pasted query. | 
				
			|||
Shift + Return | Create new link. | 
				
			|||
Shift + Backspace | Delete currently selected link. | 
				
			|||
 | 
				
			|||
## FAQ | 
				
			|||
 | 
				
			|||
### The Link I Want Is Showing Up Last | 
				
			|||
Change the effective name of the link. | 
				
			|||
Let's say you've been typing `in` for `instagram`, but recently added `indeed` as a link, | 
				
			|||
and `indeed` keeps showing up first. | 
				
			|||
Simply change the effective name of the `instagram` link to `in`: | 
				
			|||
``` | 
				
			|||
instagram `in instagram.com | 
				
			|||
``` | 
				
			|||
 | 
				
			|||
### A Link Is Blocking My Search | 
				
			|||
This happens very rarely if at all. Just throw some spaces at the end of your query. | 
				
			|||
 | 
				
			|||
### The Quick Search Function Is Stopping Me From Finishing My Query | 
				
			|||
Just type a single space before you paste in text. | 
				
			|||
 | 
				
			|||
### My Localhost Link Isn't Working | 
				
			|||
If you want to make a link that points to `localhost`, you likely need to specify the `http` protocol when creating your link. | 
				
			|||
 | 
				
			|||
## Bang List | 
				
			|||
Website | Bang Text | 
				
			|||
-|- | 
				
			|||
youtube | !youtube ~y https://www.youtube.com/results?search_query= | 
				
			|||
amazon | !amazon amazon.com/s?k= | 
				
			|||
google site:reddit.com | !google reddit https://www.google.com/search?q=site%3Areddit.com%20 | 
				
			|||
google site:reddit.com | !google stackoverflow https://www.google.com/search?q=site%3Astackoverflow.com%20 | 
				
			|||
I have big plans for fuzzyhome but don't have time to act on them | 
				
			|||
right now. As things stand it works really well and is a huge | 
				
			|||
productivity booster if you use it properly. I haven't | 
				
			|||
experienced any runtime errors using it dozens or hundreds of | 
				
			|||
times a day for months now, so it seems to be pretty stable. | 
				
			|||
Export your links just in case :) | 
				
			|||
 | 
				
			|||
@ -1 +0,0 @@ | 
				
			|||
printf "\033c" && rg --pcre2 -g '!api.imba' '(?<!api.)'"$1" | 
				
			|||
@ -1 +0,0 @@ | 
				
			|||
printf "\033c" && rg --pcre2 -g '!config.imba' 'config\.(?!data.)'"$1" | 
				
			|||
@ -0,0 +1,137 @@ | 
				
			|||
import state from './state.imba' | 
				
			|||
 | 
				
			|||
import config from './config.imba' | 
				
			|||
import { find, omit, orderBy } from 'lodash' | 
				
			|||
import fzi from 'fzi' | 
				
			|||
import { cloneDeep } from 'lodash' | 
				
			|||
import mexp from 'math-expression-evaluator' | 
				
			|||
 | 
				
			|||
export default new class api | 
				
			|||
 | 
				
			|||
	def pin_link link | 
				
			|||
		Pins[link.url] ^= 1 | 
				
			|||
		sort_links! | 
				
			|||
		global.chrome.storage.sync.set {pins:Pins} | 
				
			|||
		imba.commit! | 
				
			|||
 | 
				
			|||
	def increment_link_frequency link | 
				
			|||
		Frequencies[link.url] ??= 0 | 
				
			|||
		Frequencies[link.url] += 1 | 
				
			|||
		global.chrome.storage.sync.set {frequencies:Frequencies} | 
				
			|||
 | 
				
			|||
	def get-link-from-node node | 
				
			|||
			return unless let url = node..url | 
				
			|||
			let split_text = node.title.split	/\s+/ | 
				
			|||
			let alias | 
				
			|||
			let last = split_text[-1] | 
				
			|||
			if last.startsWith("(")	and	last.endsWith(")") | 
				
			|||
				alias = split_text.pop!.slice(1,-1) | 
				
			|||
			let name = split_text.join(" ") | 
				
			|||
			let is_bang = no | 
				
			|||
			if name.startsWith "!" | 
				
			|||
				is_bang = yes | 
				
			|||
				name = name.slice(1) | 
				
			|||
			{ name, alias, is_bang, url } | 
				
			|||
 | 
				
			|||
	def traverse stack | 
				
			|||
		const links = [] | 
				
			|||
		while stack.length > 0 | 
				
			|||
				const node = stack.pop! | 
				
			|||
				const link = get-link-from-node(node) | 
				
			|||
				links.push(link) if link | 
				
			|||
				node..children..forEach do stack.push $1 | 
				
			|||
		links | 
				
			|||
 | 
				
			|||
	def bfs title, queue | 
				
			|||
		while queue.length > 0 | 
				
			|||
			const node = queue.shift! | 
				
			|||
			return node.children if node.title.toLowerCase! is title.toLowerCase! | 
				
			|||
			if node.children | 
				
			|||
				queue = queue.concat(node.children) | 
				
			|||
 | 
				
			|||
	def sort_links | 
				
			|||
		if state.query.trim!.length <= 0 | 
				
			|||
			const pinned = do Pins[$1.url] or no | 
				
			|||
			const freq = do Frequencies[$1.url] or 0 | 
				
			|||
			state.sorted_links = orderBy(state.links, [pinned, freq], ['desc', 'desc']) | 
				
			|||
		else | 
				
			|||
			state.sorted_links = fzi.search state.query, state.links, (do $1.name), (do $1.alias) | 
				
			|||
 | 
				
			|||
	def parse_url url | 
				
			|||
		throw "invalid url" if url === null | 
				
			|||
		let get_url = do |s| | 
				
			|||
			let url = new URL s | 
				
			|||
			throw _ unless (url.host and url.href) | 
				
			|||
			url | 
				
			|||
		try | 
				
			|||
			return get_url url | 
				
			|||
		try | 
				
			|||
			return get_url "https://{url}" | 
				
			|||
		throw "invalid url" | 
				
			|||
 | 
				
			|||
	def get_pretty_date | 
				
			|||
		Date!.toString!.split(" ").slice(0, 4).join(" ") | 
				
			|||
 | 
				
			|||
	get selected_link | 
				
			|||
		state.sorted_links[state.link_selection_index] | 
				
			|||
 | 
				
			|||
	def set_link_selection_index index | 
				
			|||
		state.link_selection_index = index | 
				
			|||
 | 
				
			|||
	def increment_link_selection_index | 
				
			|||
		set_link_selection_index Math.min(state.sorted_links.length - 1, state.link_selection_index + 1) | 
				
			|||
 | 
				
			|||
	def decrement_link_selection_index | 
				
			|||
		set_link_selection_index Math.max(0, state.link_selection_index - 1) | 
				
			|||
 | 
				
			|||
	def navigate link | 
				
			|||
		await increment_link_frequency link | 
				
			|||
		window.location.href = link.url | 
				
			|||
 | 
				
			|||
	get math_result | 
				
			|||
		try | 
				
			|||
			mexp.eval(state.query) | 
				
			|||
		catch | 
				
			|||
			no | 
				
			|||
 | 
				
			|||
	def handle_cut e | 
				
			|||
		return unless e.target.selectionStart == e.target.selectionEnd | 
				
			|||
		let s = math_result | 
				
			|||
		s ||= state.query | 
				
			|||
		await window.navigator.clipboard.writeText(s) | 
				
			|||
		state.query = '' | 
				
			|||
		sort_links! | 
				
			|||
 | 
				
			|||
	def handle_click_link | 
				
			|||
		let link = selected_link | 
				
			|||
		if link.is_bang | 
				
			|||
			state.query = '' | 
				
			|||
			state.active_bang = link | 
				
			|||
		else | 
				
			|||
			navigate link | 
				
			|||
 | 
				
			|||
	get bang | 
				
			|||
		state.active_bang or config.data.default_bang | 
				
			|||
 | 
				
			|||
	get encoded_bang_query | 
				
			|||
		"{bang.url}{window.encodeURIComponent(state.query)}" | 
				
			|||
 | 
				
			|||
	get encoded_bang_query_nourl | 
				
			|||
		"{window.encodeURIComponent(state.query)}" | 
				
			|||
 | 
				
			|||
	def handle_bang | 
				
			|||
		return if state.loading | 
				
			|||
		await increment_link_frequency bang | 
				
			|||
		window.location.href = encoded_bang_query | 
				
			|||
 | 
				
			|||
	def unset_active_bang | 
				
			|||
		state.active_bang = no | 
				
			|||
		sort_links! | 
				
			|||
 | 
				
			|||
	def get-icon url | 
				
			|||
		let { host } = parse_url url | 
				
			|||
		"https://icon.horse/icon/{host}" | 
				
			|||
 | 
				
			|||
	def help | 
				
			|||
		let url = "https://github.com/familyfriendlymikey/fuzzyhome" | 
				
			|||
		window.open url,'_blank' | 
				
			|||
| 
		 Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 269 B  | 
| 
		 Before Width: | Height: | Size: 268 B After Width: | Height: | Size: 268 B  | 
| 
		 Before Width: | Height: | Size: 291 B After Width: | Height: | Size: 291 B  | 
| 
		 Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 365 B  | 
| 
		 Before Width: | Height: | Size: 308 B After Width: | Height: | Size: 308 B  | 
| 
		 Before Width: | Height: | Size: 1011 B After Width: | Height: | Size: 1011 B  | 
| 
		 Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 339 B  | 
| 
		 Before Width: | Height: | Size: 356 B After Width: | Height: | Size: 356 B  | 
| 
		 Before Width: | Height: | Size: 368 B After Width: | Height: | Size: 368 B  | 
| 
		 Before Width: | Height: | Size: 299 B After Width: | Height: | Size: 299 B  | 
@ -0,0 +1,63 @@ | 
				
			|||
tag app-bang | 
				
			|||
 | 
				
			|||
	get tips | 
				
			|||
		let result = [] | 
				
			|||
		let temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: api.handle_bang.bind(api) | 
				
			|||
				hotkey_handler: api.handle_bang.bind(api) | 
				
			|||
				hotkey: 'return' | 
				
			|||
				hotkey_display_name: "Return" | 
				
			|||
				content: "Search" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: api.handle_cut.bind(api) | 
				
			|||
		} | 
				
			|||
		if api.math_result | 
				
			|||
			temp.hotkey_display_name = "Cut Math" | 
				
			|||
			temp.content = "Cut Math Result" | 
				
			|||
		else | 
				
			|||
			temp.hotkey_display_name = "Cut" | 
				
			|||
			temp.content = "Cut All Text" | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		if state.active_bang | 
				
			|||
			temp = { | 
				
			|||
					click_handler: api.unset_active_bang.bind(api) | 
				
			|||
					hotkey_handler: api.unset_active_bang.bind(api) | 
				
			|||
					hotkey: 'esc' | 
				
			|||
					hotkey_display_name: "Esc" | 
				
			|||
					content: "Back" | 
				
			|||
			} | 
				
			|||
			result.push temp | 
				
			|||
 | 
				
			|||
		result | 
				
			|||
 | 
				
			|||
	<self> | 
				
			|||
		css w:100% d:flex fld:column gap:15px ofy:hidden | 
				
			|||
 | 
				
			|||
		<app-tips$tips tips=tips> | 
				
			|||
 | 
				
			|||
		unless $tips.show_more | 
				
			|||
 | 
				
			|||
			<.bang .selected [c:$bang-c] @click=api.handle_bang> | 
				
			|||
				css d:flex fld:row jc:space-between ai:center | 
				
			|||
					px:16px py:11px rd:5px c:$text-c | 
				
			|||
 | 
				
			|||
				<.link-left> | 
				
			|||
					css d:flex fl:1 ofy:hidden | 
				
			|||
 | 
				
			|||
					<img.link-icon src=api.get-icon(api.bang.url)> | 
				
			|||
						css w:20px h:20px mr:10px rd:3px | 
				
			|||
 | 
				
			|||
					<.display-name> "...{api.encoded_bang_query_nourl}" | 
				
			|||
						css fs:20px of:hidden text-overflow:ellipsis | 
				
			|||
 | 
				
			|||
				<.link-right> | 
				
			|||
					css d:flex fld:row jc:space-between ai:center | 
				
			|||
 | 
				
			|||
					<.frequency> api.bang.frequency | 
				
			|||
						css fs:15px ml:7px | 
				
			|||
@ -0,0 +1,77 @@ | 
				
			|||
tag app-home | 
				
			|||
 | 
				
			|||
	def mount | 
				
			|||
		$home-input.focus! | 
				
			|||
 | 
				
			|||
	def blur | 
				
			|||
		setTimeout(&, 100) do $home-input.focus! | 
				
			|||
 | 
				
			|||
	def handle_click_copy s | 
				
			|||
		try | 
				
			|||
			await window.navigator.clipboard.writeText(s) | 
				
			|||
			state.query = '' | 
				
			|||
			api.sort_links! | 
				
			|||
 | 
				
			|||
	def handle_input | 
				
			|||
		api.set_link_selection_index 0 | 
				
			|||
		api.sort_links! | 
				
			|||
 | 
				
			|||
	<self> | 
				
			|||
		css w:100% d:flex fld:column ofy:hidden gap:20px | 
				
			|||
 | 
				
			|||
		<.header> | 
				
			|||
			css | 
				
			|||
				d:hflex | 
				
			|||
 | 
				
			|||
				.side | 
				
			|||
					c:$button-c fs:15px d:box e:300ms | 
				
			|||
 | 
				
			|||
					> | 
				
			|||
						w:15px d:flex | 
				
			|||
 | 
				
			|||
				.left | 
				
			|||
					pr:15px | 
				
			|||
					jc:left | 
				
			|||
 | 
				
			|||
				.right | 
				
			|||
					pl:15px | 
				
			|||
					jc:right | 
				
			|||
 | 
				
			|||
			<.side.left @click=(state.view = 'settings')> | 
				
			|||
				<svg src="../assets/settings.svg"> | 
				
			|||
 | 
				
			|||
			<input$home-input | 
				
			|||
				autofocus | 
				
			|||
				bind=state.query | 
				
			|||
				@input=handle_input | 
				
			|||
				@cut=api.handle_cut | 
				
			|||
				disabled=state.loading | 
				
			|||
				@blur=blur | 
				
			|||
			> | 
				
			|||
				css h:50px px:20px fl:1 | 
				
			|||
					fs:20px ta:center | 
				
			|||
					bd:1px solid $input-bc | 
				
			|||
					outline:none rd:5px | 
				
			|||
					bg:$input-bg c:$text-c | 
				
			|||
					caret-color:$input-caret-c | 
				
			|||
 | 
				
			|||
			<.side.right@click=handle_click_copy(api.math_result or 0)> | 
				
			|||
				<$math> | 
				
			|||
					css d:box fs:17px min-width:min-content | 
				
			|||
					"{Math.round(api.math_result * 100)/100}" | 
				
			|||
 | 
				
			|||
				css e:200ms eaf:circ-out | 
				
			|||
					max-width:{$math.offsetWidth or 30}px | 
				
			|||
					min-width:{$math.offsetWidth}px | 
				
			|||
 | 
				
			|||
		if state.loaded | 
				
			|||
			<div ease> | 
				
			|||
				css e:400ms of:hidden | 
				
			|||
					@off o:0 | 
				
			|||
 | 
				
			|||
				if state.active_bang or state.sorted_links.length < 1 | 
				
			|||
					<app-bang> | 
				
			|||
 | 
				
			|||
				else | 
				
			|||
					<app-links> | 
				
			|||
 | 
				
			|||
@ -0,0 +1,55 @@ | 
				
			|||
tag app-link | 
				
			|||
 | 
				
			|||
	frequency = 0 | 
				
			|||
 | 
				
			|||
	<self | 
				
			|||
		@pointerover=api.set_link_selection_index(index) | 
				
			|||
		@click=api.handle_click_link | 
				
			|||
		.selected=(index is state.link_selection_index) | 
				
			|||
	> | 
				
			|||
		css d:flex fld:row jc:space-between ai:center | 
				
			|||
			px:16px py:11px rd:5px c:$text-c | 
				
			|||
		if link.is_bang | 
				
			|||
			css c:$bang-c | 
				
			|||
 | 
				
			|||
		<.link-left> | 
				
			|||
			css d:flex w:100% | 
				
			|||
 | 
				
			|||
			<img.link-icon src=api.get-icon(link.url)> | 
				
			|||
 | 
				
			|||
				css w:20px h:20px mr:10px rd:3px | 
				
			|||
 | 
				
			|||
			<.name> link.name | 
				
			|||
				css tt:capitalize fs:20px overflow-wrap:anywhere | 
				
			|||
 | 
				
			|||
			if link.alias | 
				
			|||
				<.name> | 
				
			|||
					css d:flex ja:center c:$effective-name-c ml:10px fs:14px | 
				
			|||
					css .parens fs:10px c:$effective-name-parens-c | 
				
			|||
 | 
				
			|||
					<span.parens> "(" | 
				
			|||
					<span> link.alias | 
				
			|||
					<span.parens> ")" | 
				
			|||
 | 
				
			|||
		<.link-right> | 
				
			|||
			css d:hflex jc:space-between w:70px ai:center | 
				
			|||
 | 
				
			|||
			css .selected .link-button visibility:visible | 
				
			|||
 | 
				
			|||
			<.link-buttons> | 
				
			|||
				css d:flex fld:row jc:start ai:center gap:5px | 
				
			|||
 | 
				
			|||
				css .link-button visibility:hidden rd:3px c:$button-c fs:15px px:3px | 
				
			|||
				if index is state.link_selection_index | 
				
			|||
					css .link-button visibility:visible | 
				
			|||
 | 
				
			|||
				css .link-button svg w:15px | 
				
			|||
 | 
				
			|||
				<.link-button @click.prevent.stop=api.pin_link(link)> | 
				
			|||
					if Pins[link.url] | 
				
			|||
						css visibility:visible c:$button-dim-c | 
				
			|||
 | 
				
			|||
					<svg src='../assets/star.svg'> | 
				
			|||
 | 
				
			|||
			<.frequency> Frequencies[link.url] or 0 | 
				
			|||
				css fs:15px ml:7px | 
				
			|||
@ -0,0 +1,55 @@ | 
				
			|||
tag app-links | 
				
			|||
 | 
				
			|||
	get tips | 
				
			|||
		let result = [] | 
				
			|||
		let temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
			click_handler: api.handle_click_link.bind(api) | 
				
			|||
			hotkey_handler: api.handle_click_link.bind(api) | 
				
			|||
			hotkey: 'return' | 
				
			|||
			hotkey_display_name: 'Return' | 
				
			|||
		} | 
				
			|||
		temp.content = api.selected_link.is_bang ? "Use Bang" : "Navigate To Link" | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: api.handle_cut.bind(api) | 
				
			|||
		} | 
				
			|||
		if api.math_result | 
				
			|||
			temp.hotkey_display_name = "Cut Math" | 
				
			|||
			temp.content = "Cut Math Result" | 
				
			|||
		else | 
				
			|||
			temp.hotkey_display_name = "Cut" | 
				
			|||
			temp.content = "Cut All Text" | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: api.increment_link_selection_index.bind(api) | 
				
			|||
				hotkey_handler: api.increment_link_selection_index.bind(api) | 
				
			|||
				hotkey: 'down' | 
				
			|||
				hotkey_display_name: "Down Arrow" | 
				
			|||
				content: "Move Selection Down" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		temp = { | 
				
			|||
				click_handler: api.decrement_link_selection_index.bind(api) | 
				
			|||
				hotkey_handler: api.decrement_link_selection_index.bind(api) | 
				
			|||
				hotkey: 'up' | 
				
			|||
				hotkey_display_name: "Up Arrow" | 
				
			|||
				content: "Move Selection Up" | 
				
			|||
		} | 
				
			|||
		result.push temp | 
				
			|||
 | 
				
			|||
		result | 
				
			|||
 | 
				
			|||
	<self> | 
				
			|||
		css w:100% d:flex fld:column gap:15px ofy:hidden max-height:100% | 
				
			|||
 | 
				
			|||
		<app-tips$tips tips=tips> | 
				
			|||
 | 
				
			|||
		<.links> | 
				
			|||
			css ofy:auto | 
				
			|||
			for link, index in state.sorted_links | 
				
			|||
				<app-link link=link index=index> | 
				
			|||
@ -0,0 +1,53 @@ | 
				
			|||
tag app-settings | 
				
			|||
 | 
				
			|||
	<self> | 
				
			|||
		css w:100% | 
				
			|||
 | 
				
			|||
		css .settings-container | 
				
			|||
			d:flex fld:row jc:space-around ai:center | 
				
			|||
			w:100% h:50px | 
				
			|||
			mt:10px | 
				
			|||
			gap:10px | 
				
			|||
 | 
				
			|||
		css .settings-button | 
				
			|||
			bg:none bd:none fs:14px d:box fl:1 | 
				
			|||
			rd:5px tt:uppercase | 
				
			|||
			transition:background 100ms | 
				
			|||
			h:100% px:5px | 
				
			|||
			of:hidden text-overflow:ellipsis white-space:nowrap | 
				
			|||
			bg:$button-bg c:$button-c | 
				
			|||
			@hover bg:$button-hover-bg | 
				
			|||
 | 
				
			|||
		<.settings-container> | 
				
			|||
 | 
				
			|||
			<.settings-button | 
				
			|||
				@click=(state.view = 'home') | 
				
			|||
				@hotkey("esc") | 
				
			|||
			> "BACK" | 
				
			|||
 | 
				
			|||
		<.settings-container> | 
				
			|||
 | 
				
			|||
			<.settings-button @click=api.help> | 
				
			|||
				"HELP" | 
				
			|||
 | 
				
			|||
			<.settings-button @click=config.cycle_theme> | 
				
			|||
				"THEME: {config.data.theme.toUpperCase!}" | 
				
			|||
 | 
				
			|||
		if config.data.theme is 'timed' | 
				
			|||
 | 
				
			|||
			<.settings-container> | 
				
			|||
 | 
				
			|||
				<.settings-button@click=config.set_timed_theme_start> | 
				
			|||
					"light theme start: {config.data.timed_theme_start}" | 
				
			|||
 | 
				
			|||
				<.settings-button@click=config.set_timed_theme_end> | 
				
			|||
					"light theme end: {config.data.timed_theme_end}" | 
				
			|||
 | 
				
			|||
		<.settings-container> | 
				
			|||
 | 
				
			|||
			<.settings-button @click=config.toggle_focus> | 
				
			|||
				"FOCUS ON OPEN: {config.data.focus}" | 
				
			|||
 | 
				
			|||
			<.settings-button @click=config.set_default_bang> | 
				
			|||
				"change default bang" | 
				
			|||
 | 
				
			|||
@ -0,0 +1,85 @@ | 
				
			|||
import { chunk, fill } from 'lodash' | 
				
			|||
 | 
				
			|||
tag app-tip | 
				
			|||
 | 
				
			|||
	<self | 
				
			|||
		@click.if(tip.click_handler)=tip.click_handler | 
				
			|||
	> | 
				
			|||
		css d:flex fld:column jc:start fl:1 | 
				
			|||
			bdr:1px solid | 
				
			|||
			bc:$tip-bc | 
				
			|||
			min-width:0 ta:center p:10px | 
				
			|||
			transition:background 100ms | 
				
			|||
			@first ta:left rdl:3px | 
				
			|||
			@last ta:right bd:none rdr:3px | 
				
			|||
			@hover bg:$tip-hover-c | 
				
			|||
		if tip.placeholder or not tip.click_handler | 
				
			|||
			css | 
				
			|||
				@hover @important bg:none | 
				
			|||
 | 
				
			|||
		if tip.hotkey_handler and tip.hotkey | 
				
			|||
			<@hotkey(tip.hotkey).force=tip.hotkey_handler> | 
				
			|||
				css d:none | 
				
			|||
 | 
				
			|||
		<.tip-hotkey> tip.hotkey_display_name | 
				
			|||
			css fs:12px c:$tip-hotkey-c | 
				
			|||
 | 
				
			|||
		<.tip-content> tip.content | 
				
			|||
			css pt:2px fs:14px c:$tip-content-c | 
				
			|||
 | 
				
			|||
tag app-tips | 
				
			|||
 | 
				
			|||
	@observable tips | 
				
			|||
 | 
				
			|||
	def unmount | 
				
			|||
		show_more = no | 
				
			|||
 | 
				
			|||
	def toggle | 
				
			|||
		show_more = not show_more | 
				
			|||
 | 
				
			|||
	def pad arr | 
				
			|||
		let i = arr.length | 
				
			|||
		while i < 3 | 
				
			|||
			arr.push { placeholder: yes } | 
				
			|||
			i += 1 | 
				
			|||
 | 
				
			|||
	@computed get chunks | 
				
			|||
		let chunks = chunk(tips, 3) | 
				
			|||
		pad(chunks[-1]) | 
				
			|||
		chunks | 
				
			|||
 | 
				
			|||
	<self> | 
				
			|||
		css d:flex fld:column gap:15px max-height:75% | 
				
			|||
 | 
				
			|||
		css .tip-row | 
				
			|||
			d:flex fld:row w:100% fl:1 | 
				
			|||
			fs:20px fs:14px | 
				
			|||
			jc:end ta:center | 
				
			|||
 | 
				
			|||
		<.tip-row> | 
				
			|||
			for tip in chunks[0] | 
				
			|||
				<app-tip tip=tip> | 
				
			|||
 | 
				
			|||
		if chunks.length > 1 | 
				
			|||
 | 
				
			|||
			<@click=toggle> | 
				
			|||
				css w:100% d:flex ja:center c:$button-c rdb:4px | 
				
			|||
					transition:background 100ms | 
				
			|||
					@hover bg:$tip-hover-c | 
				
			|||
				if show_more | 
				
			|||
					css rd:0 | 
				
			|||
 | 
				
			|||
				<svg src="../assets/chevron-down.svg"> | 
				
			|||
					css w:15px transition:transform 150ms | 
				
			|||
					if show_more | 
				
			|||
						css transform:rotate(180deg) | 
				
			|||
 | 
				
			|||
			# hotkeys depend on the presence of tips in the dom so | 
				
			|||
			# can't ease this as is | 
				
			|||
			<.more [d:none]=!show_more> | 
				
			|||
				css d:flex fld:column gap:15px ofy:auto e:300ms | 
				
			|||
 | 
				
			|||
				for row in chunks.slice(1) | 
				
			|||
					<.tip-row> | 
				
			|||
						for tip in row | 
				
			|||
							<app-tip tip=tip> | 
				
			|||
@ -0,0 +1,71 @@ | 
				
			|||
export default new class config | 
				
			|||
 | 
				
			|||
	def save | 
				
			|||
		global.localStorage.fuzzyhome_config = JSON.stringify(data) | 
				
			|||
 | 
				
			|||
	def constructor | 
				
			|||
		data = {} | 
				
			|||
 | 
				
			|||
		try | 
				
			|||
			data = JSON.parse(global.localStorage.fuzzyhome_config) | 
				
			|||
 | 
				
			|||
		data.focus ??= yes | 
				
			|||
 | 
				
			|||
		data.theme ??= "timed" | 
				
			|||
 | 
				
			|||
		data.timed_theme_start ??= 8 | 
				
			|||
		data.timed_theme_end ??= 18 | 
				
			|||
 | 
				
			|||
		data.default_bang ??= {} | 
				
			|||
		data.default_bang.name ??= "" | 
				
			|||
		data.default_bang.url ??= "https://www.google.com/search?q=" | 
				
			|||
		data.default_bang.frequency ??= 0 | 
				
			|||
		data.default_bang.is_bang ??= yes | 
				
			|||
		save! | 
				
			|||
 | 
				
			|||
	def cycle_theme | 
				
			|||
		if data.theme is "dark" | 
				
			|||
			data.theme = "light" | 
				
			|||
		elif data.theme is "light" | 
				
			|||
			data.theme = "timed" | 
				
			|||
		else | 
				
			|||
			data.theme = "dark" | 
				
			|||
		save! | 
				
			|||
 | 
				
			|||
	def set_default_bang | 
				
			|||
		let res = window.prompt('Insert a new search URL. For example:\nhttps://search.brave.com/search?q=')..trim! | 
				
			|||
		return unless res | 
				
			|||
		data.default_bang.url = res | 
				
			|||
		save! | 
				
			|||
 | 
				
			|||
	def toggle_focus | 
				
			|||
		data.focus = !data.focus | 
				
			|||
		save! | 
				
			|||
 | 
				
			|||
	def set_timed_theme_end | 
				
			|||
		let res = parseInt(window.prompt!) | 
				
			|||
		return unless res | 
				
			|||
		return unless res > 0 | 
				
			|||
		return unless res < 24 | 
				
			|||
		data.timed_theme_end = res | 
				
			|||
		save! | 
				
			|||
 | 
				
			|||
	def set_timed_theme_start | 
				
			|||
		let res = parseInt(window.prompt!) | 
				
			|||
		return unless res | 
				
			|||
		return unless res > 0 | 
				
			|||
		return unless res < 24 | 
				
			|||
		data.timed_theme_start = res | 
				
			|||
		save! | 
				
			|||
 | 
				
			|||
	get theme | 
				
			|||
		if data.theme is "light" | 
				
			|||
			"light" | 
				
			|||
		elif data.theme is "timed" | 
				
			|||
			let hour = new Date!.getHours! | 
				
			|||
			if hour > data.timed_theme_end or hour < data.timed_theme_start | 
				
			|||
				"dark" | 
				
			|||
			else | 
				
			|||
				"light" | 
				
			|||
		else | 
				
			|||
			"dark" | 
				
			|||
@ -0,0 +1 @@ | 
				
			|||
import "./main.imba" | 
				
			|||
@ -0,0 +1,76 @@ | 
				
			|||
global.L = console.log | 
				
			|||
 | 
				
			|||
import pkg from '../package.json' | 
				
			|||
let version = pkg.version | 
				
			|||
L "fuzzyhome version {version}" | 
				
			|||
 | 
				
			|||
import state from './state.imba' | 
				
			|||
import api from './api.imba' | 
				
			|||
import config from './config.imba' | 
				
			|||
 | 
				
			|||
import './components/app-home.imba' | 
				
			|||
import './components/app-settings.imba' | 
				
			|||
import './components/app-links.imba' | 
				
			|||
import './components/app-link.imba' | 
				
			|||
import './components/app-bang.imba' | 
				
			|||
import './components/app-tips.imba' | 
				
			|||
import './styles.imba' | 
				
			|||
 | 
				
			|||
extend tag element | 
				
			|||
	get state | 
				
			|||
		state | 
				
			|||
	get api | 
				
			|||
		api | 
				
			|||
	get config | 
				
			|||
		config | 
				
			|||
 | 
				
			|||
if config.data.focus and location.search =? "?x" | 
				
			|||
	throw new Error | 
				
			|||
 | 
				
			|||
global.Pins = {} | 
				
			|||
global.Frequencies = {} | 
				
			|||
 | 
				
			|||
def init | 
				
			|||
	let { pins } = await global.chrome.storage.sync.get 'pins' | 
				
			|||
	Pins = pins or {} | 
				
			|||
 | 
				
			|||
	let { frequencies } = await global.chrome.storage.sync.get 'frequencies' | 
				
			|||
	Frequencies = frequencies or {} | 
				
			|||
 | 
				
			|||
	global.chrome.bookmarks.getTree! do(bookmarks) | 
				
			|||
		const folder = api.bfs 'Bookmarks Bar', bookmarks | 
				
			|||
		state.links = api.traverse folder | 
				
			|||
		api.sort_links! | 
				
			|||
		state.loaded = yes | 
				
			|||
		imba.commit! | 
				
			|||
 | 
				
			|||
init! | 
				
			|||
 | 
				
			|||
tag app | 
				
			|||
 | 
				
			|||
	<self | 
				
			|||
		.light=(config.theme is "light") | 
				
			|||
		.dark=(config.theme is "dark") | 
				
			|||
		.disabled=state.loading | 
				
			|||
		ease | 
				
			|||
	> | 
				
			|||
		css d:flex fld:column jc:start ai:center | 
				
			|||
			m:0 w:100% h:100% bg:$bodybg | 
				
			|||
			ff:sans-serif fw:1 | 
				
			|||
			us:none | 
				
			|||
			e:100ms | 
				
			|||
			@off o:0 | 
				
			|||
 | 
				
			|||
		<.main> | 
				
			|||
			css d:flex fld:column jc:start ai:center | 
				
			|||
				bg:$appbg | 
				
			|||
				w:80vw max-width:700px max-height:80vh | 
				
			|||
				bxs:0px 0px 10px rgba(0,0,0,0.35) | 
				
			|||
				box-sizing:border-box p:30px rd:10px mt:10vh | 
				
			|||
 | 
				
			|||
			if state.view is 'settings' | 
				
			|||
				<app-settings> | 
				
			|||
			else | 
				
			|||
				<app-home> | 
				
			|||
 | 
				
			|||
imba.mount <app> | 
				
			|||
@ -1,9 +1,10 @@ | 
				
			|||
export default { | 
				
			|||
	view: 'home' | 
				
			|||
	query: '' | 
				
			|||
	links: [] | 
				
			|||
	sorted_links: [] | 
				
			|||
	loading: no | 
				
			|||
	link_selection_index: 0 | 
				
			|||
	active_bang: no | 
				
			|||
	bang_selection_index: -1 | 
				
			|||
	loaded:no | 
				
			|||
} | 
				
			|||
@ -0,0 +1,7 @@ | 
				
			|||
import { imba } from 'vite-plugin-imba'; | 
				
			|||
import { defineConfig } from 'vite'; | 
				
			|||
 | 
				
			|||
export default defineConfig({ | 
				
			|||
	base: '', | 
				
			|||
	plugins: [imba()], | 
				
			|||
}); | 
				
			|||