Navigate chrome bookmarks (with folder support) #1462
Replies: 3 comments 1 reply
-
Hi there. I wrote this chromemarks Python library a while ago and I think especially the platform-specific Chrome paths would come in handy to improve this script. There are also some models here that could help make this type-safe and offer additional features. HTH :) |
Beta Was this translation helpful? Give feedback.
-
I wonder what the icons were supposed to be: The
line doesn't appear to render properly... |
Beta Was this translation helpful? Give feedback.
-
Here's an improved version that includes Favicons using SQLite access to the Chrome Database, some adjustmens so I could use it with the Vivaldi Browser, strong typings, and other small improvements. Hope you like it! // Name: Bookmarks
// Keyword: bm
import '@johnlindquist/kit'
import { Choice } from '@johnlindquist/kit'
import { join } from 'path'
import sqlite3 from 'sqlite3'
//#region Chromium Bookmark Types
export interface Root {
checksum: string
roots: Roots
}
export interface Roots {
bookmark_bar: Folder
other: Folder
synced: Folder
trash: Folder
}
interface Base {
id: string
date_added: string
date_last_used: string
date_modified?: string
guid: string
meta_info?: MetaInfo
name: string
}
export interface Folder extends Base {
type: 'folder'
children: (Folder | Bookmark)[]
url: undefined
}
export interface Bookmark extends Base {
type: 'url'
url: string
children: undefined
}
export interface MetaInfo {
Description?: string
Thumbnail?: string
power_bookmark_meta?: string
Nickname?: string
ThemeColor?: string
Partner?: string
Bookmarkbar?: string
}
//#endregion
const SUPPORTED_BROWSERS = ['Chrome', 'Vivaldi'] as const
type SupportedBrowser = (typeof SUPPORTED_BROWSERS)[number]
const cacheDefaults = { favicons: null as { [url: string]: string } | null }
const cache = await db(cacheDefaults)
const browserKind = (await env('BOOKMARKS_BROWSER_KIND', {
choices: [...SUPPORTED_BROWSERS],
name: 'Which Browser do you use?',
})) as SupportedBrowser
const { bookmarksJsonFile, faviconsDbFile } = getBrowserInstallationFiles(browserKind)
const favicons = await loadFavicons(faviconsDbFile)
const root = (await readJson(bookmarksJsonFile)) as Root
let bookmarks = root.roots.bookmark_bar.children
// Initializing an array to keep track of the navigation history
let historyStack = []
type OptionValue = 'go-back' | Folder | Bookmark
function buildChoices() {
const createChoice = (item: Folder | Bookmark) => {
if (item.type === 'folder') {
return {
name: item.name,
// Folder icon (can't put it in the code due to a Kit bug)
html: `📁 ${item.name}`,
value: item,
} as Choice<OptionValue>
}
return {
name: item.name,
description: item.url,
keyword: item.meta_info?.Nickname,
img: favicons[item.url],
value: item,
} satisfies Choice<OptionValue>
}
// Generating options based on current level of bookmarks
let choices: Choice<OptionValue>[] = bookmarks.map(createChoice)
// Adding a "go back" option if there is a history in the stack
if (historyStack.length > 0) {
choices = [{ name: '⤴️ ..', description: 'Go back', value: 'go-back' }, ...choices]
}
return choices
}
// Loop to handle user interaction and navigation within bookmarks
while (true) {
const lastSelection = await arg(
{
name: 'Select A Bookmark!',
shortcuts: [
{
name: 'Update Favicons',
visible: true,
bar: 'right' as const,
key: 'ctrl+u',
onPress: async () => {
await loadFavicons(faviconsDbFile, false)
setChoices(buildChoices())
setName('Select A Bookmark!')
},
},
],
},
buildChoices(),
)
if (lastSelection === 'go-back') {
bookmarks = historyStack.pop()
continue
}
const { type, name } = lastSelection
if (type === 'folder') {
// push the old bookmarks into the stack
historyStack.push(bookmarks)
bookmarks = bookmarks.find((bookmark) => bookmark.name === name).children
continue
}
if (type === 'url') {
exec(`open "${lastSelection.url}"`)
break
}
console.log('Unknown type', type)
}
function getBrowserInstallationFiles(browserKind: SupportedBrowser) {
const platform: string = process.platform.toLowerCase()
const installDir = (() => {
switch (true) {
case browserKind === 'Chrome' && platform.includes('linux'):
return home('.config', 'google-chrome', 'Default')
case browserKind === 'Chrome' && platform.includes('darwin'):
return home('Library', 'Application Support', 'Google', 'Chrome', 'Default')
case browserKind === 'Chrome' && platform.includes('win32'):
return home('AppData', 'Local', 'Google', 'Chrome', 'User Data', 'Default')
case browserKind === 'Vivaldi' && platform.includes('win32'):
return home('AppData', 'Local', 'Vivaldi', 'User Data', 'Default')
default:
throw new Error(
`Not implemented: It is unknown where the ${browserKind} bookmarks path for platform ${process.platform} is. Please help us out with a pull request!`,
)
}
})()
if (!pathExistsSync(installDir)) {
throw new Error(
`${browserKind} bookmarks path determined to be at '${installDir}' according to system platform, but nothing exists at that location. Is ${browserKind} installed?`,
)
}
return {
bookmarksJsonFile: join(installDir, 'Bookmarks'),
faviconsDbFile: join(installDir, 'Favicons'),
} as const
}
async function loadFavicons(faviconsDbFile: string, allowCached = true) {
if (allowCached && cache.favicons) {
return cache.favicons
}
setHint('Loading Favicons...');
const db = new sqlite3.Database(faviconsDbFile, sqlite3.OPEN_READONLY, (err) => {
if (err) {
throw err
}
})
type BookmarkFavicon = { page_url: string; image_data: Buffer }
const result = await new Promise<BookmarkFavicon[] | 'locked'>((resolve, reject) => {
db.all<{ page_url: string; image_data: Buffer }>(
'SELECT page_url, image_data FROM icon_mapping INNER JOIN favicons ON favicons.id = icon_mapping.icon_id INNER JOIN main.favicon_bitmaps fb on favicons.id = fb.icon_id',
[],
async (err, rows) => {
if (err) {
if ('code' in err && err.code === 'SQLITE_BUSY') {
resolve('locked')
}
reject(err)
}
resolve(rows)
},
)
})
db.close()
setHint(undefined);
if (result === 'locked') {
return await databaseLockedPrompt(faviconsDbFile)
}
const b64Map = result.reduce((agg, row) => {
const { page_url, image_data } = row
const b64 = `data:image/jpeg;base64,${image_data.toString('base64')}`
agg.set(page_url, b64)
return agg
}, new Map<string, string>())
const b64Data = Object.fromEntries(b64Map.entries())
cache.favicons = b64Data
cache.write().then()
return b64Data
}
async function databaseLockedPrompt(faviconsDbFile: string) {
const choice = await select(
{
hint: 'Cannot read the Favicons database while the browser is still running. Please close it completely to cache the Favicons and continue to try again.',
multiple: false,
},
[
{ name: 'Retry', value: 'retry' },
{ name: 'Continue without Favicons', value: 'without' },
],
)
setHint(null)
switch (choice) {
case 'without':
return new Map<string, string>()
case 'retry':
return await loadFavicons(faviconsDbFile, false)
}
} |
Beta Was this translation helpful? Give feedback.
-
Install script in Script Kit
Beta Was this translation helpful? Give feedback.
All reactions