Skip to content

Instantly share code, notes, and snippets.

@Strajk
Created February 9, 2025 18:36
Show Gist options
  • Save Strajk/079180aabdef9484200bcb3adf22ef8a to your computer and use it in GitHub Desktop.
Save Strajk/079180aabdef9484200bcb3adf22ef8a to your computer and use it in GitHub Desktop.
// Name: Flashcards
// Description: Space-repeat images, managed just by filesystem
// Shortcut: shift+ctrl+option+cmd+o
import '@johnlindquist/kit'
const flashcardsDir = kenvPath('db', 'flashcards')
await ensureDir(flashcardsDir)
const today = new Date()
let files = await readdir(flashcardsDir)
let renamed = 0
for (const file of files) {
if (!file.endsWith('.png')) continue
const parsed = parseFilename(file)
if (!parsed) {
renamed++
const max = currentMaxForDate(formatDate(today))
const newName = formatFilename(formatDate(today), max + 1, 0)
await rename(path.join(flashcardsDir, file), path.join(flashcardsDir, newName))
}
}
if (renamed > 0) {
notify(`Flashcards: Renamed ${renamed} files to match the flashcard format`)
files = await readdir(flashcardsDir) // refresh files after rename
}
let cards = files
.filter(file => file.endsWith('.png'))
.map(file => {
const parsed = parseFilename(file)!
return {
filename: file,
...parsed,
fullPath: path.join(flashcardsDir, file),
}
})
if (cards.length === 0) {
open(`file://${flashcardsDir}`)
await div(md(`
## No flashcards found in ${flashcardsDir}
Just add images (must be pngs!) there (I've opened the folder for you).
Don't worry about the filename, it will be renamed automatically on the next run.
After you are done, just rerun this script
`))
exit()
}
cards.sort((a, b) => {
if (a.nextReviewDate < formatDate(today) && b.nextReviewDate >= formatDate(today)) return -1
if (a.nextReviewDate >= formatDate(today) && b.nextReviewDate < formatDate(today)) return 1
return a.orderInBucket - b.orderInBucket
})
const reviewDays = [1, 2, 4, 7, 14, 30]
const shortcuts = {
"remember": 'r',
"forgot": 'f',
"delete": 'd',
}
const cardsToReview = cards.filter(card => {
const reviewDate = new Date(card.nextReviewDate)
return reviewDate <= today
})
const { size } = await getActiveScreen()
for (const card of cardsToReview) {
const days = reviewDays[Math.min(card.numberOfReviewsDone, reviewDays.length - 1)]
const nextReview = new Date()
nextReview.setDate(today.getDate() + days)
await div({
x: 50,
y: 50,
height: size.height - 100,
width: size.width - 100,
enter: "Close",
hint: `${cardsToReview.length} cards to review`,
html: `<img
src="file://${card.fullPath}"
style="width: 100%; height: 100%; object-fit: contain"
/>`,
className: "h-full w-full", // Otherwise the image could grow too much and cause y scroll
shortcuts: [
// Right bar
{
name: "Remember",
key: "Space",
bar: "right",
onPress: async () => {
const newFilename = formatFilename(
formatDate(nextReview),
card.orderInBucket,
card.numberOfReviewsDone + 1
)
await rename(
card.fullPath,
path.join(flashcardsDir, newFilename)
)
submit(null)
},
},
// Left bar
{
name: "Forgot",
key: shortcuts.forgot,
bar: "left",
onPress: async () => {
const lastCard = cards.reduce((max, card) => Math.max(max, card.orderInBucket), 0)
const newFilename = formatFilename(formatDate(today), lastCard + 1, 0)
await rename(card.fullPath, path.join(flashcardsDir, newFilename))
submit(null)
},
},
{
name: "Delete",
key: shortcuts.delete,
bar: "left",
onPress: async () => {
await trash(card.fullPath)
submit(null)
},
},
{
name: "Reveal in Finder",
key: "r",
bar: "left",
onPress: async () => {
// -R reveals the file in Finder instead of opening it
await exec(`open -R "${card.fullPath}"`)
},
},
],
})
}
say(`Time flows like water,
Knowledge grows with each review,
Mind blooms day by day`)
await div(
md(`
# That's all folks!
All flashcards have been reviewed.
See ya next time!
`)
)
// Helpers
// ===
function parseFilename(filename: string) {
const match = filename.match(/^CARD_(\d{4}-\d{2}-\d{2})_(\d+)_(\d+)\.png$/)
if (!match) return null
return {
nextReviewDate: match[1],
orderInBucket: parseInt(match[2], 10),
numberOfReviewsDone: parseInt(match[3], 10),
}
}
function formatFilename(nextReviewDate: string,
orderInBucket: number,
numberOfReviewsDone: number) {
return `CARD_${nextReviewDate}_${orderInBucket}_${numberOfReviewsDone}.png`
}
function currentMaxForDate(date: string) {
const files = readdirSync(flashcardsDir)
return files.filter(file => file.startsWith(`CARD_${date}_`)).map(file => parseFilename(file)!.orderInBucket).reduce((max, order) => Math.max(max, order), 0)
}
function formatDate(date: Date) {
return date.toISOString().slice(0, 10)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment