Browse Source

switch to vite and chrome

main
familyfriendlymikey 1 year ago
parent
commit
d610633ec9
  1. 1
      .gitignore
  2. 1
      .rgignore
  3. 318
      app/api.imba
  4. 14
      app/assets/community_links.imba
  5. 1
      app/assets/eye-off.svg
  6. 115
      app/client.imba
  7. 129
      app/components/app-bang.imba
  8. 131
      app/components/app-community-links.imba
  9. 84
      app/components/app-edit.imba
  10. 73
      app/components/app-home.imba
  11. 67
      app/components/app-link.imba
  12. 104
      app/components/app-links.imba
  13. 16
      app/components/app-prompt.imba
  14. 153
      app/components/app-settings.imba
  15. 86
      app/components/app-tips.imba
  16. 59
      app/config.imba
  17. 51
      app/db.imba
  18. 41
      app/sw.imba
  19. 13
      app/utils.imba
  20. 13
      chrome/manifest.json
  21. 5
      index.html
  22. 4469
      package-lock.json
  23. 31
      package.json
  24. 166
      readme.md
  25. 1
      scripts/api_move.sh
  26. 1
      scripts/config_move.sh
  27. 137
      src/api.imba
  28. 0
      src/assets/chevron-down.svg
  29. 0
      src/assets/chevron-up.svg
  30. 0
      src/assets/edit-2.svg
  31. 2
      src/assets/help-circle.svg
  32. 0
      src/assets/search.svg
  33. 0
      src/assets/settings.svg
  34. 0
      src/assets/star.svg
  35. 0
      src/assets/trash.svg
  36. 0
      src/assets/x-square.svg
  37. 0
      src/assets/x.svg
  38. 63
      src/components/app-bang.imba
  39. 77
      src/components/app-home.imba
  40. 55
      src/components/app-link.imba
  41. 55
      src/components/app-links.imba
  42. 53
      src/components/app-settings.imba
  43. 85
      src/components/app-tips.imba
  44. 71
      src/config.imba
  45. 1
      src/index.js
  46. 76
      src/main.imba
  47. 3
      src/state.imba
  48. 30
      src/styles.imba
  49. 0
      src/utils.imba
  50. 7
      vite.config.js

1
.gitignore

@ -3,3 +3,4 @@ node_modules
dist
*.swp
.fdignore
*.zip

1
.rgignore

@ -0,0 +1 @@
.fdignore

318
app/api.imba

@ -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!

14
app/assets/community_links.imba

@ -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'

1
app/assets/eye-off.svg

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye-off"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>

Before

Width:  |  Height:  |  Size: 460 B

115
app/client.imba

@ -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>

129
app/components/app-bang.imba

@ -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

131
app/components/app-community-links.imba

@ -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

84
app/components/app-edit.imba

@ -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>

73
app/components/app-home.imba

@ -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>

67
app/components/app-link.imba

@ -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

104
app/components/app-links.imba

@ -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>

16
app/components/app-prompt.imba

@ -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)

153
app/components/app-settings.imba

@ -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"

86
app/components/app-tips.imba

@ -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>

59
app/config.imba

File diff suppressed because one or more lines are too long

51
app/db.imba

@ -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

41
app/sw.imba

@ -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))

13
app/utils.imba

@ -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!

13
chrome/manifest.json

@ -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"
}
}

5
app/index.html → index.html

@ -4,9 +4,8 @@
<title>&lrm;</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style src='*'></style>
</head>
<body>
<script type="module" src="./client.imba"></script>
<body style="background:#20222f">
<script type="module" src="/src/index.js"></script>
</body>
</html>

4469
package-lock.json

File diff suppressed because it is too large

31
package.json

@ -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"
}
}

166
readme.md

@ -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
scripts/api_move.sh

@ -1 +0,0 @@
printf "\033c" && rg --pcre2 -g '!api.imba' '(?<!api.)'"$1"

1
scripts/config_move.sh

@ -1 +0,0 @@
printf "\033c" && rg --pcre2 -g '!config.imba' 'config\.(?!data.)'"$1"

137
src/api.imba

@ -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'

0
app/assets/chevron-down.svg → src/assets/chevron-down.svg

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

0
app/assets/chevron-up.svg → src/assets/chevron-up.svg

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 268 B

0
app/assets/edit-2.svg → src/assets/edit-2.svg

Before

Width:  |  Height:  |  Size: 291 B

After

Width:  |  Height:  |  Size: 291 B

2
app/assets/eye.svg → src/assets/help-circle.svg

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>

Before

Width:  |  Height:  |  Size: 316 B

After

Width:  |  Height:  |  Size: 365 B

0
app/assets/search.svg → src/assets/search.svg

Before

Width:  |  Height:  |  Size: 308 B

After

Width:  |  Height:  |  Size: 308 B

0
app/assets/settings.svg → src/assets/settings.svg

Before

Width:  |  Height:  |  Size: 1011 B

After

Width:  |  Height:  |  Size: 1011 B

0
app/assets/star.svg → src/assets/star.svg

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 339 B

0
app/assets/trash.svg → src/assets/trash.svg

Before

Width:  |  Height:  |  Size: 356 B

After

Width:  |  Height:  |  Size: 356 B

0
app/assets/x-square.svg → src/assets/x-square.svg

Before

Width:  |  Height:  |  Size: 368 B

After

Width:  |  Height:  |  Size: 368 B

0
app/assets/x.svg → src/assets/x.svg

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 299 B

63
src/components/app-bang.imba

@ -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

77
src/components/app-home.imba

@ -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>

55
src/components/app-link.imba

@ -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

55
src/components/app-links.imba

@ -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>

53
src/components/app-settings.imba

@ -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"

85
src/components/app-tips.imba

@ -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>

71
src/config.imba

@ -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"

1
src/index.js

@ -0,0 +1 @@
import "./main.imba"

76
src/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>

3
app/state.imba → src/state.imba

@ -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
}

30
app/styles.imba → src/styles.imba

@ -1,3 +1,12 @@
global css html
$effective-name-c:gray4
$effective-name-parens-c:gray4/80
bg:#20222f
global css body
m:0 bd:0 p:0
bg:#20222f
global css .dark
$appbg:#20222f
$bodybg:#20222f
@ -36,30 +45,11 @@ global css .light
$button-bg:blue4/10
$button-hover-bg:blue4/20
global css html
$effective-name-c:gray4
$effective-name-parens-c:gray4/80
bg:#20222f
global css body
m:0 bd:0 p:0
bg:#20222f
global css input
w:100% h:50px px:20px fl:1
fs:20px ta:center
bd:1px solid
outline:none rd:5px
bg:$input-bg
bc:$input-bc
c:$text-c
caret-color:$input-caret-c
global css a
td:none
global css .disabled *
@important c:gray4 cursor:default user-select:none pointer-events:none
@important c:gray4 user-select:none pointer-events:none
global css .disabled $main-input
@important bg:gray4/10 bc:gray4

0
src/utils.imba

7
vite.config.js

@ -0,0 +1,7 @@
import { imba } from 'vite-plugin-imba';
import { defineConfig } from 'vite';
export default defineConfig({
base: '',
plugins: [imba()],
});
Loading…
Cancel
Save