├── .github
├── media
│ ├── demo.gif
│ └── logo.png
└── workflows
│ └── push.yml
├── .gitignore
├── Makefile
├── Makefile.ios
├── README.md
├── Remember.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
└── xcshareddata
│ └── xcschemes
│ ├── remember-ios.xcscheme
│ └── remember.xcscheme
├── bin
├── download-macos-artifacts
├── pbracket
├── pbraco
├── remove-dylibs
└── sign-dylibs
├── core
├── .gitignore
├── appdata.rkt
├── command.rkt
├── database.rkt
├── dylib.entitlements
├── entry.rkt
├── event.rkt
├── info.rkt
├── main.rkt
├── ring.rkt
├── schema.rkt
├── tag.rkt
├── timezone.rkt
└── undo.rkt
├── manual
├── index.scrbl
├── info.rkt
└── shortcut.rkt
├── migrations
├── 0001-create-entries-table.sql
├── 0002-create-entries-status-due-at-index.sql
├── 0003-create-tags-table.sql
├── 0004-create-entry-tags-table.sql
├── 0005-add-recurrence-columns-to-entries.sql
├── 0006-add-entry-updated-at-column.sql
└── 0007-add-entry-tags-unique-index.sql
├── remember-ios
├── AddReminderIntent.swift
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ └── icon.png
│ └── Contents.json
├── Backend.swift
├── BackendExtensions.swift
├── CommandField.swift
├── CommandView.swift
├── ContentView.swift
├── DeviceShakeViewModifier.swift
├── FolderSyncer.swift
├── Info.plist
├── NotificationsManager.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── PrivacyInfo.xcprivacy
├── Remember.swift
├── ReminderDetailView.swift
├── RemindersView.swift
├── SettingsSyncView.swift
├── SettingsView.swift
└── Store.swift
├── remember
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── 1024x1024@3x-1024.png
│ │ ├── 1024x1024@3x-128.png
│ │ ├── 1024x1024@3x-16.png
│ │ ├── 1024x1024@3x-256.png
│ │ ├── 1024x1024@3x-32.png
│ │ ├── 1024x1024@3x-512.png
│ │ ├── 1024x1024@3x-64.png
│ │ └── Contents.json
│ ├── Contents.json
│ ├── Icon.imageset
│ │ ├── 32x32.png
│ │ ├── 32x32@2x.png
│ │ ├── 32x32@3x.png
│ │ └── Contents.json
│ ├── OnboardingStep1-1.imageset
│ │ ├── Contents.json
│ │ └── OnboardingStep1-1.png
│ ├── OnboardingStep1-2.imageset
│ │ ├── Contents.json
│ │ └── OnboardingStep1-2.png
│ ├── OnboardingStep1-3.imageset
│ │ ├── Contents.json
│ │ └── OnboardingStep1-3.png
│ ├── OnboardingStep2-1.imageset
│ │ ├── Contents.json
│ │ └── OnboardingStep2-1.png
│ ├── OnboardingStep2-2.imageset
│ │ ├── Contents.json
│ │ └── OnboardingStep2-2.png
│ └── StatusBarIcon.imageset
│ │ ├── 16x16.png
│ │ ├── 32x32.png
│ │ ├── 64x64.png
│ │ └── Contents.json
├── Backend.swift
├── BackendExtensions.swift
├── Base.lproj
│ └── Main.storyboard
├── BridgingHeader.h
├── CommandField.swift
├── ContentView.swift
├── EntryList.swift
├── EntryListItem.swift
├── FolderSync.swift
├── Info.plist
├── KeyboardShortcut.swift
├── KeyboardShortcutDefaults.swift
├── KeyboardShortcutField.swift
├── Keycode.swift
├── Notifications.swift
├── Onboarding.swift
├── Preferences.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Snooze.swift
├── StatusItem.swift
├── Store.swift
├── UserNotifications.swift
├── VisualEffectBackground.swift
├── remember-core.entitlements
├── remember.entitlements
└── vendor
│ └── DDHotKey
│ ├── DDHotKeyCenter.h
│ ├── DDHotKeyCenter.m
│ ├── DDHotKeyTextField.h
│ ├── DDHotKeyTextField.m
│ ├── DDHotKeyUtilities.h
│ ├── DDHotKeyUtilities.m
│ └── README.markdown
├── tests
├── info.rkt
└── remember
│ ├── command.rkt
│ ├── entry.rkt
│ ├── ring.rkt
│ └── undo.rkt
└── website
├── .gitattributes
├── Makefile
├── assets
├── .gitattributes
├── Makefile
├── app-store.svg
├── demo-adding.gif
├── demo-adding.mp4
├── demo-listing.gif
├── demo-listing.mp4
├── demo-notify.gif
├── demo-notify.mp4
├── demo.gif
├── demo.mp4
└── logo.png
├── index.html
└── manual
├── index.html
├── manual-fonts.css
├── manual-racket.css
├── manual-racket.js
├── manual-style.css
├── racket.css
├── scribble-common.js
└── scribble.css
/.github/media/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/.github/media/demo.gif
--------------------------------------------------------------------------------
/.github/media/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/.github/media/logo.png
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | on: push
2 | name: CI
3 | jobs:
4 | build_core:
5 | runs-on: ${{ matrix.environment }}
6 | name: Build core (${{ matrix.platform }}) on ${{ matrix.environment }}
7 | strategy:
8 | matrix:
9 | environment: [macos-13, macos-14]
10 | include:
11 | - environment: macos-13
12 | platform: x86_64
13 | arch: x64
14 | - environment: macos-14
15 | platform: arm64
16 | arch: arm64
17 | steps:
18 | - uses: actions/checkout@master
19 | - uses: Bogdanp/setup-racket@v1.11
20 | with:
21 | architecture: ${{ matrix.arch }}
22 | distribution: 'full'
23 | variant: 'CS'
24 | version: '8.13'
25 | packages: http-easy-lib
26 | - name: Clone Noise
27 | run: |
28 | mkdir ../../sandbox
29 | env GIT_LFS_SKIP_SMUDGE=1 \
30 | git clone --depth 1 --branch racket-8.13 https://github.com/Bogdanp/Noise ../../sandbox/Noise
31 | raco pkg install -D --batch --auto ../../sandbox/noise/Racket/noise-serde-lib/
32 | - name: Install core
33 | run: |
34 | raco pkg install -D --batch --auto --name remember core/
35 | raco pkg install -D --batch --auto --name remember-test tests/
36 | - name: Run tests
37 | run: raco test tests/
38 | - name: Build manual
39 | run: make remember/res/manual/index.html
40 | - name: Build core
41 | run: make
42 | - name: Show your work
43 | run: find remember/res
44 | - name: Upload manual
45 | uses: actions/upload-artifact@v4
46 | with:
47 | name: manual-${{ matrix.platform }}
48 | path: remember/res/manual/
49 | - name: Upload core
50 | uses: actions/upload-artifact@v4
51 | with:
52 | name: core-${{ matrix.platform }}
53 | path: |
54 | remember/res/core-${{ matrix.platform }}.zo
55 | remember/res/runtime-${{ matrix.platform }}/
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | compiled/
3 | res/
4 | temp/
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | ARCH=$(shell uname -m)
2 |
3 | APP_SRC=remember
4 | RKT_SRC=core
5 | RKT_FILES=$(shell find ${RKT_SRC} -name '*.rkt')
6 | RKT_MAIN_ZO=${RKT_SRC}/compiled/main_rkt.zo
7 |
8 | RESOURCES_PATH=${APP_SRC}/res
9 | RUNTIME_NAME=runtime-${ARCH}
10 | RUNTIME_PATH=${RESOURCES_PATH}/${RUNTIME_NAME}
11 | MANUAL_PATH=${RESOURCES_PATH}/manual
12 |
13 | CORE_ZO=${RESOURCES_PATH}/core-${ARCH}.zo
14 |
15 | .PHONY: all
16 | all: ${CORE_ZO} ${APP_SRC}/Backend.swift
17 |
18 | .PHONY: clean
19 | clean:
20 | find core -type d -name compiled | xargs rm -fr
21 | rm -fr ${RESOURCES_PATH}
22 |
23 | ${RKT_MAIN_ZO}: ${RKT_FILES}
24 | raco make -j 16 -v ${RKT_SRC}/main.rkt
25 |
26 | ${CORE_ZO}: ${RKT_MAIN_ZO}
27 | mkdir -p ${RESOURCES_PATH}
28 | rm -fr ${RUNTIME_PATH}
29 | raco ctool \
30 | --runtime ${RUNTIME_PATH} \
31 | --runtime-access ${RUNTIME_NAME} \
32 | --mods $@ ${RKT_SRC}/main.rkt
33 |
34 | ${APP_SRC}/Backend.swift: ${CORE_ZO}
35 | raco noise-serde-codegen ${RKT_SRC}/main.rkt > $@
36 |
37 | ${MANUAL_PATH}/index.html: manual/*.scrbl
38 | raco scribble --html --dest ${MANUAL_PATH} +m manual/index.scrbl
39 |
40 | website/manual/index.html: manual/*.scrbl
41 | make -C website manual/index.html
42 |
--------------------------------------------------------------------------------
/Makefile.ios:
--------------------------------------------------------------------------------
1 | APP_SRC=remember-ios
2 | RKT_SRC=core
3 | RKT_FILES=$(shell find ${RKT_SRC} -name '*.rkt')
4 | RKT_MAIN_ZO=${RKT_SRC}/compiled/main_rkt.zo
5 |
6 | RESOURCES_PATH=${APP_SRC}/res
7 | RUNTIME_NAME=runtime
8 | RUNTIME_PATH=${RESOURCES_PATH}/${RUNTIME_NAME}
9 | MANUAL_PATH=${RESOURCES_PATH}/manual
10 |
11 | CORE_ZO=${RESOURCES_PATH}/core.zo
12 |
13 | .PHONY: all
14 | all: ${CORE_ZO} ${APP_SRC}/Backend.swift
15 |
16 | .PHONY: clean
17 | clean:
18 | rm -fr ${RESOURCES_PATH}
19 |
20 | ${RKT_MAIN_ZO}: ${RKT_FILES}
21 | ./bin/pbraco make -j 16 -v ${RKT_SRC}/main.rkt
22 |
23 | ${CORE_ZO}: ${RKT_MAIN_ZO}
24 | mkdir -p ${RESOURCES_PATH}
25 | rm -fr ${RUNTIME_PATH}
26 | ./bin/pbraco ctool \
27 | --runtime ${RUNTIME_PATH} \
28 | --runtime-access ${RUNTIME_NAME} \
29 | --mods $@ ${RKT_SRC}/main.rkt
30 |
31 | ${APP_SRC}/Backend.swift: ${CORE_ZO}
32 | ./bin/pbraco noise-serde-codegen ${RKT_SRC}/main.rkt > $@
33 |
34 | ${MANUAL_PATH}/index.html: manual/*.scrbl
35 | ./bin/pbraco scribble --html --dest ${MANUAL_PATH} +m manual/index.scrbl
36 |
37 | website/manual/index.html: manual/*.scrbl
38 | make -C website manual/index.html
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Remember
7 |
8 |
9 |
10 |
11 |
12 |
13 | Remember is a tool for stashing distractions away for later. You bind
14 | it to a hotkey (⌥⎵ by default) and whenever something unexpected pops
15 | up -- say you suddenly realize you need to stock up on milk -- you hit
16 | the hotkey, then type in `buy milk +1h` and hit return. An hour
17 | later, you'll get reminded that you need to go out and buy some milk.
18 |
19 |
20 |
21 |
22 |
23 | If you find Remember useful, please consider [buying a copy].
24 |
25 | This application is **not** Open Source. I'm providing the source
26 | code here because I want users to be able to see the code they're
27 | running and even change and build it for themselves if they want to.
28 | In that vein, you're free to read, build and run the application
29 | yourself, on your own devices, but please don't share any built
30 | artifacts with others.
31 |
32 | [buying a copy]: https://remember.defn.io
33 |
34 | ## Build
35 |
36 | ### Requirements
37 |
38 | * [Racket 8.12 CS](https://racket-lang.org/)
39 | * [Noise](https://github.com/Bogdanp/Noise)
40 | * macOS Catalina
41 | * Xcode 12+
42 |
43 | ### First-time Setup
44 |
45 | $ raco pkg install --name remember core/
46 |
47 | ### Building
48 |
49 | $ make
50 | $ xcodebuild
51 |
52 | ## License
53 |
54 | Copyright 2019-2024 CLEARTYPE SRL. All rights reserved.
55 |
--------------------------------------------------------------------------------
/Remember.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Remember.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Remember.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "6ebdba50b24c762f95398173d1bc6fa5c6be5062592c3f0f3ccb421f747fbcd8",
3 | "pins" : [
4 | {
5 | "identity" : "launchatlogin",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/sindresorhus/LaunchAtLogin",
8 | "state" : {
9 | "revision" : "e8171b3e38a2816f579f58f3dac1522aa39efe41",
10 | "version" : "4.2.0"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/Remember.xcodeproj/xcshareddata/xcschemes/remember-ios.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
58 |
59 |
60 |
61 |
67 |
69 |
75 |
76 |
77 |
78 |
80 |
81 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/Remember.xcodeproj/xcshareddata/xcschemes/remember.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
78 |
79 |
80 |
81 |
87 |
89 |
95 |
96 |
97 |
98 |
100 |
101 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/bin/download-macos-artifacts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env racket
2 | #lang racket/base
3 |
4 | (require file/unzip
5 | (prefix-in http: net/http-easy)
6 | racket/file
7 | racket/path
8 | threading)
9 |
10 | (define current-token
11 | (make-parameter (getenv "GH_TOKEN")))
12 | (define here
13 | (path-only (syntax-source #'here)))
14 | (define res-path
15 | (normalize-path (build-path here 'up "remember" "res")))
16 |
17 | (define get
18 | (make-keyword-procedure
19 | (lambda (kws kw-args path . args)
20 | (keyword-apply
21 | http:get
22 | kws kw-args
23 | #:auth (http:bearer-auth (current-token))
24 | (if (not (regexp-match? #rx"^https://" path))
25 | (format "https://api.github.com/~a" path)
26 | path)
27 | args))))
28 |
29 | (define (get-artifacts-by-name run-id)
30 | (define data
31 | (http:response-json
32 | (get (format "repos/Bogdanp/remember/actions/runs/~a/artifacts" run-id))))
33 | (for/hash ([elt-data (in-list (hash-ref data 'artifacts))])
34 | (values (hash-ref elt-data 'name) elt-data)))
35 |
36 | (define (download-archive uri dst-path)
37 | (define res #f)
38 | (dynamic-wind
39 | (lambda ()
40 | (set! res (get #:stream? #t uri)))
41 | (lambda ()
42 | (call-with-unzip
43 | (http:response-output res)
44 | (lambda (src-path)
45 | (for ([filename (in-list (directory-list src-path))])
46 | (copy-directory/files
47 | (build-path src-path filename)
48 | (build-path dst-path filename))))))
49 | (lambda ()
50 | (http:response-close! res))))
51 |
52 | (module+ main
53 | (require racket/cmdline)
54 | (define run-id
55 | (command-line
56 | #:args [run-id]
57 | run-id))
58 | (unless (current-token)
59 | (eprintf "error: GH_TOKEN environment variable not set~n")
60 | (exit 1))
61 | (define artifacts-by-name
62 | (get-artifacts-by-name run-id))
63 | (delete-directory/files #:must-exist? #f res-path)
64 | (make-directory* (build-path res-path "manual"))
65 | (for ([(name dst-path) (in-hash
66 | (hash
67 | "core-arm64" res-path
68 | "core-x86_64" res-path
69 | "manual-arm64" (build-path res-path "manual")))])
70 | (define uri
71 | (~> (hash-ref artifacts-by-name name)
72 | (hash-ref 'archive_download_url)))
73 | (eprintf "Downloading ~a...~n" name)
74 | (download-archive uri dst-path)))
75 |
--------------------------------------------------------------------------------
/bin/pbracket:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Helper to run Racket in cross-compilation mode with a portable
4 | # bytecode target. Expects the PBRACKET_ROOT environment variable to
5 | # be set before running.
6 |
7 | set -euo pipefail
8 |
9 | RACKET_DIR="${PBRACKET_ROOT}/racket"
10 | BUILD_DIR="${RACKET_DIR}/src/build/cs/c"
11 | COMPILED_DIR="${BUILD_DIR}/compiled"
12 |
13 | racket \
14 | --cross-compiler tpb64l "$BUILD_DIR" \
15 | -MCR "$COMPILED_DIR": \
16 | -G "${RACKET_DIR}/etc" \
17 | -X "${RACKET_DIR}/collects" \
18 | "$@"
19 |
--------------------------------------------------------------------------------
/bin/pbraco:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | HERE="$(dirname "$0")"
6 | "$HERE/pbracket" -N raco -U -l- raco "$@"
7 |
--------------------------------------------------------------------------------
/bin/remove-dylibs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | find "${PROJECT_DIR}/remember-ios/res" -name "*.dylib" -exec rm \{\} \;
6 |
--------------------------------------------------------------------------------
/bin/sign-dylibs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # On macOS, add this script as an early build phase to sign
4 | # distributed dylibs. Make sure to disable "User Script Sandboxing"
5 | # under "Build Settings" > "Build Options" in the Xcode target
6 | # configuration.
7 |
8 | set -euo pipefail
9 |
10 | while read -r path; do
11 | filename=$(basename "$path")
12 | libname=$(echo "$filename" | cut -d. -f1)
13 | codesign --remove-signature "$path"
14 | codesign \
15 | --timestamp \
16 | --entitlements="${PROJECT_DIR}/core/dylib.entitlements" \
17 | --sign "${EXPANDED_CODE_SIGN_IDENTITY}" \
18 | -i "${PRODUCT_BUNDLE_IDENTIFIER}.${libname}" \
19 | -o runtime \
20 | "$path"
21 | done < <(find "${PROJECT_DIR}/remember/res" -name '*.dylib')
22 |
--------------------------------------------------------------------------------
/core/.gitignore:
--------------------------------------------------------------------------------
1 | compiled
2 |
--------------------------------------------------------------------------------
/core/appdata.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require racket/file)
4 |
5 | (provide
6 | current-application-data-directory
7 | build-application-path)
8 |
9 | (define app-id "io.defn.remember")
10 |
11 | (define (current-application-data-directory)
12 | (case (system-type 'os)
13 | [(macosx)
14 | (build-path
15 | (find-system-path 'home-dir)
16 | "Library"
17 | "Application Support"
18 | app-id)]
19 |
20 | [else
21 | (error 'current-application-data-directory "not implemented")]))
22 |
23 | (define (build-application-path . args)
24 | (apply build-path (current-application-data-directory) args))
25 |
26 | (make-directory* (current-application-data-directory))
27 |
--------------------------------------------------------------------------------
/core/database.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require db
4 | gregor
5 | racket/contract/base
6 | racket/file
7 | racket/format
8 | racket/list
9 | racket/path
10 | "appdata.rkt")
11 |
12 | (provide
13 | id/c
14 | isolation-level/c
15 |
16 | (contract-out
17 | [make-db (-> (-> connection?) db?)]
18 | [current-db (parameter/c db?)]
19 | [call-with-database-connection
20 | (->* [(-> connection? any)]
21 | [#:db db?]
22 | any)]
23 | [call-with-database-transaction
24 | (->* [(-> connection? any)]
25 | [#:db db?
26 | #:isolation isolation-level/c]
27 | any)])
28 | sql->
29 | backup-database!
30 | create-database-copy!
31 | merge-database-copy!)
32 |
33 | (define id/c
34 | exact-nonnegative-integer?)
35 |
36 | (define isolation-level/c
37 | (or/c #f
38 | 'serializable
39 | 'repeatable-read
40 | 'read-committed
41 | 'read-uncommitted))
42 |
43 | (struct db (conn sem)
44 | #:transparent)
45 |
46 | (define (make-db connector)
47 | (db (connector)
48 | (make-semaphore 1)))
49 |
50 | (define current-db
51 | (make-parameter
52 | (make-db
53 | (lambda ()
54 | (sqlite3-connect
55 | #:mode 'create
56 | #:database (build-application-path "remember.sqlite3")
57 | #:use-place 'os-thread)))))
58 |
59 | (define current-connection
60 | (make-parameter #f))
61 |
62 | (define (call-with-database-connection proc #:db [the-db (current-db)])
63 | (call-with-semaphore (db-sem the-db)
64 | (λ () (proc (db-conn the-db)))))
65 |
66 | (define (call-with-database-transaction proc
67 | #:db [the-db (current-db)]
68 | #:isolation [isolation #f])
69 | (cond
70 | [(current-connection)
71 | => (lambda (conn)
72 | (call-with-transaction conn
73 | (lambda ()
74 | (proc conn))))]
75 |
76 | [else
77 | (call-with-database-connection
78 | #:db the-db
79 | (lambda (conn)
80 | (parameterize ([current-connection conn])
81 | (call-with-transaction conn
82 | #:isolation isolation
83 | (lambda ()
84 | (proc conn))))))]))
85 |
86 | (define (sql-> v)
87 | (cond
88 | [(sql-null? v) #f]
89 | [else v]))
90 |
91 | (define max-backups 7)
92 |
93 | (define (delete-old-backups!)
94 | (define all-backups
95 | (sort
96 | (find-files
97 | (lambda (p)
98 | (equal? (path-get-extension p) #".bak"))
99 | (build-application-path))
100 | (lambda (a b)
101 | (bytes (path->bytes a)
102 | (path->bytes b)))))
103 |
104 | (when (> (length all-backups) max-backups)
105 | (for-each delete-file (drop-right all-backups max-backups))))
106 |
107 | (define (backup-database!)
108 | (define database-path (build-application-path "remember.sqlite3"))
109 | (when (file-exists? database-path)
110 | (define backup-suffix (~a "-" (~t (today) "yyyy-MM-dd") ".bak"))
111 | (define backup-path (path-add-extension database-path (string->bytes/utf-8 backup-suffix)))
112 | (copy-file database-path backup-path #t)
113 | (delete-old-backups!)))
114 |
115 | (define (create-database-copy!)
116 | (define path (path->string (make-temporary-file)))
117 | (begin0 path
118 | (delete-file path)
119 | (call-with-database-connection
120 | (lambda (conn)
121 | (query-exec conn "vacuum into ?" path)))))
122 |
123 | (define (merge-database-copy! path)
124 | (call-with-database-connection
125 | (lambda (conn)
126 | (dynamic-wind
127 | (lambda ()
128 | (query-exec conn "attach ? as newer_db" path))
129 | (lambda ()
130 | (call-with-transaction conn
131 | (lambda ()
132 | (query-exec conn #< DATETIME(updated_at)
149 | QUERY
150 | )
151 |
152 | (query-exec conn #<
2 |
3 |
4 |
5 |
6 | com.apple.security.app-sandbox
7 |
8 | com.apple.security.inherit
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/core/event.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require noise/backend
4 | noise/serde)
5 |
6 | (provide
7 | entries-did-change)
8 |
9 | (define-callout (entries-did-change-cb [ok : Bool]))
10 |
11 | (define ready-for-changes? #f)
12 |
13 | (define-rpc (mark-ready-for-changes)
14 | (set! ready-for-changes? #t))
15 |
16 | (define (entries-did-change)
17 | (when ready-for-changes?
18 | (entries-did-change-cb #t)))
19 |
--------------------------------------------------------------------------------
/core/info.rkt:
--------------------------------------------------------------------------------
1 | #lang info
2 |
3 | (define version "2024.04.01")
4 | (define collection "remember")
5 | (define deps '("base"
6 | "db-lib"
7 | "deta-lib"
8 | "gregor-lib"
9 | ["noise-serde-lib" #:version "0.7"]
10 | "threading-lib"))
11 |
--------------------------------------------------------------------------------
/core/main.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require gregor
4 | noise/backend
5 | noise/serde
6 | "command.rkt"
7 | "database.rkt"
8 | "entry.rkt"
9 | "schema.rkt"
10 | "timezone.rkt"
11 | "undo.rkt")
12 |
13 | (provide
14 | main)
15 |
16 | (define-rpc (ping : String)
17 | "pong")
18 |
19 | (define-rpc (parse [command s : String] : (Listof Token))
20 | (parse-command s))
21 |
22 | (define-rpc (commit [command s : String] : Entry)
23 | (entry->Entry (commit! s)))
24 |
25 | (define-rpc (update [entry-with-id id : UVarint]
26 | [and-command s : String] : (Optional Entry))
27 | (define e (update! id s))
28 | (and e (entry->Entry e)))
29 |
30 | (define-rpc (archive [entry-with-id id : UVarint])
31 | (void (archive-entry! id)))
32 |
33 | (define-rpc (snooze [entry-with-id id : UVarint]
34 | [for-minutes minutes : UVarint])
35 | (void (snooze-entry! id minutes)))
36 |
37 | (define-rpc (delete [entry-with-id id : UVarint])
38 | (void (delete-entry! id)))
39 |
40 | (define-rpc (get-pending-entries : (Listof Entry))
41 | (map entry->Entry (find-pending-entries)))
42 |
43 | (define-rpc (get-due-entries : (Listof Entry))
44 | (map entry->Entry (find-due-entries)))
45 |
46 | (define-rpc (undo)
47 | (void (undo!)))
48 |
49 | (define-rpc (create-database-copy : String)
50 | (create-database-copy!))
51 |
52 | (define-rpc (merge-database-copy [at-path path : String])
53 | (merge-database-copy! path))
54 |
55 | (define-callout (entries-due-cb [entries : (Listof Entry)]))
56 |
57 | (define-rpc (start-scheduler)
58 | (void (do-start-scheduler)))
59 |
60 | (define scheduler-custodian
61 | (make-custodian))
62 |
63 | (define (do-start-scheduler)
64 | (parameterize ([current-custodian scheduler-custodian])
65 | (thread
66 | (lambda ()
67 | (let loop ()
68 | (define deadline (+ (current-inexact-milliseconds) 30000))
69 | (define due-entries (get-due-entries))
70 | (unless (null? due-entries)
71 | (entries-due-cb due-entries))
72 | (sync (alarm-evt deadline))
73 | (loop))))))
74 |
75 | (define (main in-fd out-fd)
76 | (current-timezone (get-current-system-timezone))
77 | (module-cache-clear!)
78 | (backup-database!)
79 | (migrate!)
80 | (let/cc trap
81 | (parameterize ([exit-handler
82 | (lambda (err-or-code)
83 | (when (exn:fail? err-or-code)
84 | ((error-display-handler)
85 | (format "trap: ~a" (exn-message err-or-code))
86 | err-or-code))
87 | (trap))])
88 | (define stop
89 | (serve in-fd out-fd))
90 | (with-handlers ([exn:break? void])
91 | (sync never-evt))
92 | (stop))))
93 |
--------------------------------------------------------------------------------
/core/ring.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require racket/contract/base)
4 |
5 | (provide
6 | (contract-out
7 | [make-ring (-> exact-positive-integer? ring?)]
8 | [ring? (-> any/c boolean?)]
9 | [ring-push! (-> ring? any/c void?)]
10 | [ring-pop! (-> ring? (or/c #f any/c))]
11 | [ring-size (-> ring? exact-nonnegative-integer?)]))
12 |
13 | (struct ring (sema vs cap [pos #:mutable] [size #:mutable])
14 | #:transparent)
15 |
16 | (define (make-ring cap)
17 | (ring (make-semaphore 1)
18 | (make-vector cap #f)
19 | cap
20 | 0
21 | 0))
22 |
23 | (define (ring-push! r v)
24 | (call-with-semaphore (ring-sema r)
25 | (lambda ()
26 | (define vs (ring-vs r))
27 | (define cap (ring-cap r))
28 | (define pos (ring-pos r))
29 | (vector-set! vs pos v)
30 | (set-ring-pos! r (modulo (add1 pos) cap))
31 | (set-ring-size! r (min cap (add1 (ring-size r)))))))
32 |
33 | (define (ring-pop! r)
34 | (call-with-semaphore (ring-sema r)
35 | (lambda ()
36 | (cond
37 | [(zero? (ring-size r)) #f]
38 | [else
39 | (define vs (ring-vs r))
40 | (define cap (ring-cap r))
41 | (define pos (if (zero? (ring-pos r))
42 | (sub1 cap)
43 | (sub1 (ring-pos r))))
44 | (begin0 (vector-ref vs pos)
45 | (vector-set! vs pos #f)
46 | (set-ring-pos! r pos)
47 | (set-ring-size! r (sub1 (ring-size r))))]))))
48 |
--------------------------------------------------------------------------------
/core/schema.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require (for-syntax racket/base)
4 | db
5 | racket/file
6 | racket/list
7 | racket/path
8 | racket/runtime-path
9 | racket/string
10 | "database.rkt")
11 |
12 | (provide
13 | migrate!)
14 |
15 | (define-runtime-path migrations-path
16 | (build-path 'up "migrations"))
17 |
18 | (define migration-paths
19 | (sort
20 | (find-files
21 | (lambda (p)
22 | (string-suffix? (path->string p) ".sql"))
23 | (normalize-path migrations-path))
24 | (lambda (a b)
25 | (string-ci (path->string a)
26 | (path->string b)))))
27 |
28 | (define (migrate!)
29 | (call-with-database-connection
30 | (lambda (conn)
31 | (query-exec conn "create table if not exists schema_migrations(ref text not null unique)")
32 | (for ([migration-path (in-list migration-paths)])
33 | (define ref (path->string (last (explode-path migration-path))))
34 | (unless (query-maybe-value conn "select true from schema_migrations where ref = $1" ref)
35 | ;; This is pretty piggy but db-lib doesn't support multiple statements per query.
36 | (define migration (file->string migration-path))
37 | (define statements (string-split migration ";\n"))
38 | (for ([statement (in-list statements)])
39 | (when (non-empty-string? (string-trim statement))
40 | (query-exec conn statement)))
41 | (query-exec conn "insert into schema_migrations values($1)" ref))))))
42 |
--------------------------------------------------------------------------------
/core/tag.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require db
4 | deta
5 | racket/contract/base
6 | racket/string
7 | threading
8 | "database.rkt")
9 |
10 | (provide
11 | (contract-out
12 | [assign-tags! (-> id/c (listof non-empty-string?) void?)]
13 | [find-tags-by-entry-id (-> connection? id/c (listof non-empty-string?))]))
14 |
15 | (define-schema tag
16 | #:table "tags"
17 | ([id id/f #:primary-key #:auto-increment]
18 | [name string/f #:contract non-empty-string? #:wrapper string-trim]))
19 |
20 | (define-schema entry-tag
21 | #:table "entry_tags"
22 | ([entry-id id/f]
23 | [tag-id id/f]))
24 |
25 | (define (assign-tags! entry-id tags)
26 | (unless (null? tags)
27 | (call-with-database-transaction
28 | (lambda (conn)
29 | (for ([name (in-list tags)])
30 | (define t
31 | (or (~> (from tag #:as t)
32 | (where (= t.name ,name))
33 | (limit 1)
34 | (lookup conn _))
35 | (~> (make-tag #:name name)
36 | (insert-one! conn _))))
37 | (unless (~> (from entry-tag #:as et)
38 | (where (and (= et.entry-id ,entry-id)
39 | (= et.tag-id ,(tag-id t))))
40 | (lookup conn _))
41 | (~> (make-entry-tag #:entry-id entry-id
42 | #:tag-id (tag-id t))
43 | (insert-one! conn _))))))))
44 |
45 | (define (find-tags-by-entry-id conn id)
46 | (~> (from entry-tag #:as et)
47 | (join tag #:as t #:on (= t.id et.tag-id))
48 | (select t.name)
49 | (where (= et.entry-id ,id))
50 | (query-list conn _)))
51 |
--------------------------------------------------------------------------------
/core/timezone.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require ffi/unsafe/nsstring
4 | ffi/unsafe/objc)
5 |
6 | (provide
7 | get-current-system-timezone)
8 |
9 | (import-class NSTimeZone)
10 |
11 | ;; On iOS, gregor has trouble determining the system timezone, so let's
12 | ;; always just ask the system directly instead.
13 | (define (get-current-system-timezone)
14 | (tell #:type _NSString
15 | (tell NSTimeZone systemTimeZone)
16 | name))
17 |
--------------------------------------------------------------------------------
/core/undo.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require racket/contract/base
4 | "ring.rkt")
5 |
6 | (provide
7 | (contract-out
8 | [current-undo-ring (parameter/c ring?)]
9 | [push-undo! (-> (-> any) void?)]
10 | [undo! (-> void?)]))
11 |
12 | (define current-undo-ring
13 | (make-parameter (make-ring 128)))
14 |
15 | (define (push-undo! proc)
16 | (ring-push! (current-undo-ring) proc))
17 |
18 | (define (undo!)
19 | (define proc (ring-pop! (current-undo-ring)))
20 | (when proc (void (proc))))
21 |
--------------------------------------------------------------------------------
/manual/index.scrbl:
--------------------------------------------------------------------------------
1 | #lang scribble/manual
2 |
3 | @(require "shortcut.rkt")
4 |
5 | @title{Remember: Stash distractions away}
6 | @author[(author+email "Bogdan Popa" "bogdan@defn.io")]
7 |
8 | @(define (homepage-anchor text)
9 | (link "https://remember.defn.io" text))
10 |
11 | @homepage-anchor{Remember} is meant to be operated entirely via the
12 | keyboard. While very convenient once you're used to it, this makes it
13 | challenging to get started. This document describes the features
14 | available in Remember and their operation.
15 |
16 | @(define-syntax-rule (kbd sym ...)
17 | (let ([text (shortcut 'sym ...)])
18 | (elemref `("kbd" ,text) text)))
19 |
20 | @(define-syntax-rule (defkbd (sym ...) pre-content ...)
21 | (let ([text (shortcut 'sym ...)])
22 | (elem
23 | (elemtag `("kbd" ,text))
24 | text
25 | " --- "
26 | pre-content ...)))
27 |
28 | @section{Reading This Document}
29 |
30 | Because Remember is keyboard-driven, this document contains many
31 | keyboard shortcuts. Shortcuts are written using the textual
32 | representation of each key. When you see a shortcut like @kbd[opt
33 | space], you should interpret it as holding down the Option key and
34 | pressing the Space key.
35 |
36 | @tabular[
37 | #:style 'boxed
38 | #:row-properties '(bottom-border ())
39 | (list
40 | (list @bold{Name} @bold{Symbol})
41 | (list @elem{Control} @"⌃")
42 | (list @elem{Option} @"⌥")
43 | (list @elem{Command} @"⌘")
44 | (list @elem{Space} @"⎵")
45 | (list @elem{Delete} @"⌫")
46 | (list @elem{Return} @"↩")
47 | (list @elem{Escape} @"⎋"))
48 | ]
49 |
50 | @subsection{Definitions}
51 |
52 | The @deftech{input area} is the text input in which you type
53 | reminders.
54 |
55 | The @deftech{current desktop} is the desktop or screen on which the
56 | mouse cursor is currently sitting.
57 |
58 |
59 | @section{Basics}
60 |
61 | @defkbd[(opt space)]{
62 | Pressing this shortcut shows or hides Remember. If Remember is
63 | running, it will appear on the @tech{current desktop} whenever you
64 | press this shortcut.
65 |
66 | You can customize this shortcut using the Preferences pane (@kbd[cmd
67 | comma]).
68 | }
69 |
70 | @defkbd[(cmd Q)]{
71 | Pressing this shortcut quits the application.
72 | }
73 |
74 | @defkbd[(cmd comma)]{
75 | Pressing this shortcut opens the Preferences dialog.
76 | }
77 |
78 |
79 | @section{Navigation & Editing}
80 |
81 | @defkbd[(return)]{
82 | If the @tech{input area} contains text, pressing return creates a
83 | new reminder. If an existing reminder is selected, pressing return
84 | opens the reminder for editing, and pressing return again commits
85 | the change.
86 | }
87 |
88 | @defkbd[(escape)]{
89 | If the @tech{input area} contains text, pressing escape clears it.
90 | If the input area is already empty, then pressing escape dismisses
91 | Remember.
92 | }
93 |
94 | @defkbd[(ctl P)]{
95 | Selects the previous reminder.
96 | }
97 |
98 | @defkbd[(ctl N)]{
99 | Selects the next reminder.
100 | }
101 |
102 | @defkbd[(delete)]{
103 | If an existing reminder is selected, pressing delete archives it.
104 | @tech{Recurring reminders} are reset instead.
105 | }
106 |
107 | @defkbd[(opt delete)]{
108 | If an existing reminder is selected, pressing @kbd[opt delete]
109 | deletes it.
110 | }
111 |
112 | @defkbd[(cmd Z)]{
113 | Undoes the previous action.
114 | }
115 |
116 |
117 | @section{Date/time Modifiers}
118 |
119 | Remember recognizes a small set of special modifiers in your reminders
120 | that control if and when it notifies you about them. For example, a
121 | reminder like @centered{@verbatim{buy milk +30m *every 2 weeks*}}
122 | would fire 30 minutes from when it is created and then once every 2
123 | weeks at the same time of day.
124 |
125 | @subsection{Relative Modifiers}
126 |
127 | A @litchar{+} character followed by a positive number and an interval
128 | suffix instructs Remember to notify you after a specific amount of
129 | time. The supported intervals are:
130 |
131 | @itemlist[
132 | @item{@tt{m} --- for minutes, eg. @tt{+10m} meaning 10 minutes from now}
133 | @item{@tt{h} --- for hours, eg. @tt{+2h} meaning 2 hours from now}
134 | @item{@tt{d} --- for days, eg. @tt{+1d} meaning 1 day from now}
135 | @item{@tt{w} --- for weeks, eg. @tt{+1w} meaning 1 week from now}
136 | @item{@tt{M} --- for months, eg. @tt{+6M} meaning 6 months from now}
137 | ]
138 |
139 | @subsection{Exact Modifiers}
140 |
141 | A @litchar[@"@"] character followed by a time and an optional day of
142 | the week instructs Remember to notify you at an exact time of day.
143 | Some examples:
144 |
145 | @itemlist[
146 | @item{@tt[@"@10am"] --- sets a reminder for 10am}
147 | @item{@tt[@"@10:30pm"] --- sets a reminder for 10:30pm}
148 | @item{@tt[@"@22:30"] --- the same as above, but using military time}
149 | @item{@tt[@"@10am tomorrow"] --- sets a reminder for 10 the following day}
150 | @item{@tt[@"@10am tmw"] --- a shorthand for the above}
151 | @item{@tt[@"@8pm mon"] --- sets a reminder for 8pm the following Monday}
152 | ]
153 |
154 | If you don't specify a day of the week and that time has already
155 | passed in the current day, then Remember implicitly considers the
156 | reminder to be for the following day. For example, say it is
157 | currently 11am, the reminder @centered{@verbatim[@"buy milk @10am"]}
158 | will fire at 10am the next day.
159 |
160 | @subsection{Recurring Reminders}
161 |
162 | The following modifiers create @tech{recurring reminders}:
163 |
164 | @itemlist[
165 | @item{@tt{*hourly*}}
166 | @item{@tt{*daily*}}
167 | @item{@tt{*weekly*}}
168 | @item{@tt{*monthly*}}
169 | @item{@tt{*yearly*}}
170 | @item{@tt{*every N hours*}}
171 | @item{@tt{*every N days*}}
172 | @item{@tt{*every N weeks*}}
173 | @item{@tt{*every N months*}}
174 | @item{@tt{*every N years*}}
175 | ]
176 |
177 | Where @tt{N} is any positive number.
178 |
179 | @deftech{Recurring reminders} repeat at the same time of day after
180 | some interval. Archiving a recurring reminder (@kbd[delete]) resets
181 | it for the next interval.
182 |
183 |
184 | @subsection{Postponing Reminders}
185 |
186 | When editing a reminder (@kbd[return]), you can postpone it by
187 | applying another modifier to it. For example, if you create a
188 | reminder like @centered{@verbatim{buy milk +20m}} and then edit it
189 | five minutes later to add another modifier like
190 | @centered{@verbatim{buy milk +60m}} then the two modifiers stack up
191 | and you will be reminded about it after 75 minutes.
192 |
193 | If you make a mistake, you can undo the change with @kbd[cmd Z].
194 |
--------------------------------------------------------------------------------
/manual/info.rkt:
--------------------------------------------------------------------------------
1 | #lang info
2 |
3 | (define collection "remember")
4 | (define scribblings '(("remember.scrbl")))
5 |
--------------------------------------------------------------------------------
/manual/shortcut.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require racket/string)
4 |
5 | (provide
6 | shortcut)
7 |
8 | (define (sym->str s)
9 | (case s
10 | [(ctl) "⌃"]
11 | [(opt) "⌥"]
12 | [(cmd) "⌘"]
13 | [(space) "⎵"]
14 | [(shift) "⇧"]
15 | [(return) "↩"]
16 | [(delete) "⌫"]
17 | [(escape) "⎋"]
18 | [(comma) ","]
19 | [(up) "↑"]
20 | [(down) "↓"]
21 | [else (symbol->string s)]))
22 |
23 | (define (shortcut . syms)
24 | (define strs (map sym->str syms))
25 | (string-join strs " "))
26 |
--------------------------------------------------------------------------------
/migrations/0001-create-entries-table.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE entries(
2 | id INTEGER PRIMARY KEY AUTOINCREMENT,
3 | title TEXT NOT NULL,
4 | body TEXT NOT NULL,
5 | status TEXT NOT NULL,
6 | due_at TEXT,
7 | created_at TEXT NOT NULL
8 | );
9 |
--------------------------------------------------------------------------------
/migrations/0002-create-entries-status-due-at-index.sql:
--------------------------------------------------------------------------------
1 | CREATE INDEX entries_status_due_at_idx ON entries(status, due_at ASC)
2 |
--------------------------------------------------------------------------------
/migrations/0003-create-tags-table.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE tags(
2 | id INTEGER PRIMARY KEY AUTOINCREMENT,
3 | name TEXT UNIQUE NOT NULL
4 | );
5 |
--------------------------------------------------------------------------------
/migrations/0004-create-entry-tags-table.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE entry_tags(
2 | entry_id INTEGER REFERENCES entries(id) ON DELETE CASCADE,
3 | tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE
4 | );
5 |
--------------------------------------------------------------------------------
/migrations/0005-add-recurrence-columns-to-entries.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE entries ADD COLUMN next_recurrence_at TEXT;
2 | ALTER TABLE entries ADD COLUMN recurrence_delta INTEGER;
3 | ALTER TABLE entries ADD COLUMN recurrence_modifier INTEGER NULLABLE;
4 |
--------------------------------------------------------------------------------
/migrations/0006-add-entry-updated-at-column.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE entries ADD COLUMN updated_at TEXT NOT NULL DEFAULT '';
2 | UPDATE entries SET updated_at = STRFTIME('%Y-%m-%dT%H:%M:%f');
3 |
--------------------------------------------------------------------------------
/migrations/0007-add-entry-tags-unique-index.sql:
--------------------------------------------------------------------------------
1 | CREATE UNIQUE INDEX entry_tags_idx ON entry_tags(entry_id, tag_id);
2 |
--------------------------------------------------------------------------------
/remember-ios/AddReminderIntent.swift:
--------------------------------------------------------------------------------
1 | import AppIntents
2 | import Foundation
3 |
4 | struct AddReminderIntent: AppIntent {
5 | static var title: LocalizedStringResource = "Add a reminder"
6 |
7 | @Parameter(title: "Reminder")
8 | var reminder: String?
9 |
10 | func perform() async throws -> some ProvidesDialog {
11 | var text = self.reminder
12 | if text == nil {
13 | text = try await $reminder.requestValue("What would you like to be reminded about?")
14 | }
15 | _ = try await Backend.shared.commit(command: text!)
16 | return .result(dialog: "OK. I've added a reminder.")
17 | }
18 | }
19 |
20 | struct AddReminderShortcut: AppShortcutsProvider {
21 | static var appShortcuts: [AppShortcut] {
22 | AppShortcut(
23 | intent: AddReminderIntent(),
24 | phrases: [
25 | "Add a reminder in \(.applicationName)",
26 | "Add a reminder to \(.applicationName)",
27 | "\(.applicationName) to \(\.$reminder)",
28 | "Remind me to \(\.$reminder) in \(.applicationName)"
29 | ],
30 | shortTitle: "Add Reminder",
31 | systemImageName: "plus.app"
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/remember-ios/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/remember-ios/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/remember-ios/Assets.xcassets/AppIcon.appiconset/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember-ios/Assets.xcassets/AppIcon.appiconset/icon.png
--------------------------------------------------------------------------------
/remember-ios/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/remember-ios/BackendExtensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | // - MARK: Backend
5 | extension Backend {
6 | static let shared = Backend(
7 | withZo: Bundle.main.url(forResource: "res/core", withExtension: "zo")!,
8 | andMod: "main",
9 | andProc: "main"
10 | )
11 | }
12 |
13 | // - MARK: Entry
14 | extension Entry: Hashable, Identifiable {
15 | public func hash(into hasher: inout Hasher) {
16 | hasher.combine(id)
17 | }
18 |
19 | public static func == (lhs: Entry, rhs: Entry) -> Bool {
20 | return lhs.id == rhs.id
21 | }
22 |
23 | var notificationId: String {
24 | "io.defn.remember-ios.Entry.\(id)"
25 | }
26 | }
27 |
28 | // - MARK: Token
29 | extension Token {
30 | var color: Color {
31 | switch self.data {
32 | case .relativeTime(_, _):
33 | return .blue
34 | case .namedDatetime(_):
35 | return .blue
36 | case .namedDate(_):
37 | return .blue
38 | case .recurrence(_, _):
39 | return .green
40 | case .tag(_):
41 | return .red
42 | default:
43 | return .primary
44 | }
45 | }
46 |
47 | var range: NSRange {
48 | return NSRange(
49 | location: Int(span.lo.offset),
50 | length: Int(span.hi.offset - span.lo.offset))
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/remember-ios/CommandField.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct CommandField: UIViewRepresentable {
4 | typealias UIViewType = CommandTextField
5 |
6 | @Binding var text: String
7 |
8 | init(_ text: Binding) {
9 | _text = text
10 | }
11 |
12 | func makeUIView(context: Context) -> CommandTextField {
13 | let toolbar = UIToolbar()
14 | toolbar.barStyle = .default
15 | toolbar.items = [
16 | UIBarButtonItem(
17 | title: "@",
18 | style: .plain,
19 | target: context.coordinator,
20 | action: #selector(Coordinator.didPressToolbarAtButton(sender:))
21 | ),
22 | UIBarButtonItem(
23 | title: "+",
24 | style: .plain,
25 | target: context.coordinator,
26 | action: #selector(Coordinator.didPressToolbarPlusButton(sender:))
27 | ),
28 | UIBarButtonItem(
29 | title: "*",
30 | style: .plain,
31 | target: context.coordinator,
32 | action: #selector(Coordinator.didPressToolbarTimesButton(sender:))
33 | ),
34 | ]
35 | toolbar.isTranslucent = true
36 | toolbar.translatesAutoresizingMaskIntoConstraints = false
37 | toolbar.sizeToFit()
38 |
39 | let field = CommandTextField()
40 | field.allowsEditingTextAttributes = true
41 | field.backgroundColor = UIColor.clear
42 | field.inputAccessoryView = toolbar
43 | field.placeholder = "Remember..."
44 | field.text = text
45 | field.addTarget(
46 | context.coordinator,
47 | action: #selector(Coordinator.textFieldDidChange(sender:)),
48 | for: .editingChanged)
49 | return field
50 | }
51 |
52 | func updateUIView(_ field: CommandTextField, context: Context) {
53 | if field.text != text {
54 | field.text = text
55 | context.coordinator.scheduleHighlight(forTextField: field)
56 | }
57 | }
58 |
59 | func makeCoordinator() -> Coordinator {
60 | return Coordinator($text)
61 | }
62 |
63 | final class Coordinator: NSObject {
64 | @Binding var text: String
65 |
66 | private var timer: Timer?
67 |
68 | init(_ text: Binding) {
69 | _text = text
70 | }
71 |
72 | func scheduleHighlight(forTextField field: UITextField) {
73 | timer?.invalidate()
74 | timer = .scheduledTimer(withTimeInterval: 1.0/30.0, repeats: false) { _ in
75 | RunLoop.main.schedule { [weak self] in
76 | self?.highlight(textField: field)
77 | }
78 | }
79 | }
80 |
81 | func highlight(textField field: UITextField) {
82 | guard let text = field.text, text != "" else { return }
83 | guard let tokens = try? Backend.shared.parse(command: text).wait() else { return }
84 | let attributedText = NSMutableAttributedString(string: text)
85 | attributedText.beginEditing()
86 | for token in tokens {
87 | attributedText.addAttribute(
88 | .foregroundColor,
89 | value: UIColor(token.color),
90 | range: token.range)
91 | }
92 | attributedText.endEditing()
93 | field.attributedText = attributedText
94 | }
95 |
96 | @objc func didPressToolbarAtButton(sender: Any) {
97 | text += "@"
98 | }
99 |
100 | @objc func didPressToolbarTimesButton(sender: Any) {
101 | text += "*"
102 | }
103 |
104 | @objc func didPressToolbarPlusButton(sender: Any) {
105 | text += "+"
106 | }
107 |
108 | @objc func textFieldDidChange(sender: UITextField) {
109 | scheduleHighlight(forTextField: sender)
110 | text = sender.text ?? ""
111 | }
112 | }
113 | }
114 |
115 | // - MARK: CommandTextField
116 | class CommandTextField: UITextField {
117 | }
118 |
--------------------------------------------------------------------------------
/remember-ios/CommandView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct CommandView: View {
4 | @Environment(\.dismiss) private var dismiss
5 |
6 | @State private var command = ""
7 | @FocusState private var focused
8 |
9 | var body: some View {
10 | NavigationView {
11 | VStack(alignment: .leading) {
12 | CommandField($command)
13 | .focused($focused)
14 | .frame(width: nil, height: 48)
15 | Spacer()
16 | }
17 | .padding()
18 | .navigationTitle("New Reminder")
19 | .toolbar {
20 | ToolbarItem(placement: .cancellationAction) {
21 | Button("Cancel") {
22 | dismiss()
23 | }
24 | }
25 | ToolbarItem(placement: .confirmationAction) {
26 | Button("Add") {
27 | Backend.shared.commit(command: command).onComplete { _ in
28 | dismiss()
29 | }
30 | }
31 | }
32 | }
33 | }.onAppear {
34 | focused = true
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/remember-ios/ContentView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ContentView: View {
4 | @Environment(\.scenePhase) private var scenePhase
5 | @ObservedObject private var store = Store()
6 | @State private var tab = "home"
7 | @State private var bgTab = "home"
8 | @State private var presentSheet = false
9 |
10 | var body: some View {
11 | TabView(selection: $tab) {
12 | RemindersView(store: store)
13 | .tabItem { Image(systemName: "house.fill") }
14 | .tag("home")
15 |
16 | Text("")
17 | .tabItem { Image(systemName: "plus.app.fill") }
18 | .tag("new-reminder")
19 |
20 | SettingsView()
21 | .tabItem { Image(systemName: "gearshape.fill") }
22 | .tag("settings")
23 | }
24 | .sheet(isPresented: $presentSheet, onDismiss: {
25 | tab = bgTab
26 | }, content: {
27 | CommandView()
28 | })
29 | .onChange(of: tab) {
30 | if tab == "new-reminder" {
31 | presentSheet = true
32 | tab = bgTab
33 | } else {
34 | bgTab = tab
35 | }
36 | }
37 | .onChange(of: scenePhase) {
38 | switch scenePhase {
39 | case .background:
40 | store.invalidate()
41 | FolderSyncer.shared.invalidate()
42 | FolderSyncer.shared.scheduleRefresh()
43 | NotificationsManager.shared.scheduleRefresh()
44 | case .inactive:
45 | store.invalidate()
46 | FolderSyncer.shared.invalidate()
47 | case .active:
48 | store.scheduleLoadEntries()
49 | FolderSyncer.shared.scheduleSync()
50 | FolderSyncer.shared.unscheduleRefresh()
51 | NotificationsManager.shared.unscheduleRefresh()
52 | @unknown default:
53 | fatalError()
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/remember-ios/DeviceShakeViewModifier.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | extension UIDevice {
5 | static let deviceDidShakeNotification = Notification.Name(rawValue: "deviceDidShakeNotification")
6 | }
7 |
8 | extension UIWindow {
9 | open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
10 | if motion == .motionShake {
11 | NotificationCenter.default.post(
12 | name: UIDevice.deviceDidShakeNotification,
13 | object: event)
14 | } else {
15 | super.motionEnded(motion, with: event)
16 | }
17 | }
18 | }
19 |
20 | struct DeviceShakeViewModifier: ViewModifier {
21 | let action: (UIEvent?) -> Void
22 |
23 | func body(content: Content) -> some View {
24 | content.onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { notification in
25 | action(notification.object as? UIEvent )
26 | }
27 | }
28 | }
29 |
30 | extension View {
31 | func onShake(perform action: @escaping (UIEvent?) -> Void) -> some View {
32 | self.modifier(DeviceShakeViewModifier(action: action))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/remember-ios/FolderSyncer.swift:
--------------------------------------------------------------------------------
1 | import BackgroundTasks
2 | import Foundation
3 | import os
4 |
5 | fileprivate let logger = Logger(
6 | subsystem: "io.defn.remember",
7 | category: "FolderSyncer"
8 | )
9 |
10 | class FolderSyncer {
11 | static let shared = FolderSyncer()
12 |
13 | static private let refreshIdentifier = "io.defn.remember.FolderSyncer.refresh"
14 |
15 | init() {
16 | sync()
17 | scheduleSync()
18 | }
19 |
20 | private var timer: Timer?
21 | private var id: String {
22 | if let id = UserDefaults.standard.string(forKey: "sync.id") {
23 | return id
24 | }
25 |
26 | let id = UUID().uuidString
27 | UserDefaults.standard.set(id, forKey: "sync.id")
28 | return id
29 | }
30 |
31 | func registerTasks() {
32 | logger.debug("Registering refresh task.")
33 | BGTaskScheduler.shared.register(
34 | forTaskWithIdentifier: Self.refreshIdentifier,
35 | using: nil) { [weak self] task in
36 | logger.debug("Handling refresh.")
37 | self?.handleRefresh(task as! BGAppRefreshTask)
38 | }
39 | }
40 |
41 | func scheduleRefresh() {
42 | logger.debug("Preparing to schedule refresh.")
43 | let request = BGAppRefreshTaskRequest(identifier: Self.refreshIdentifier)
44 | let deadline = Date(timeIntervalSinceNow: 5*60)
45 | request.earliestBeginDate = deadline
46 | logger.debug("Scheduling refresh at \(deadline.ISO8601Format()).")
47 | do {
48 | try BGTaskScheduler.shared.submit(request)
49 | } catch {
50 | logger.error("Failed to schedule refresh: \(error)")
51 | }
52 | }
53 |
54 | func unscheduleRefresh() {
55 | logger.debug("Cancelling scheduled refresh tasks.")
56 | BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: Self.refreshIdentifier)
57 | }
58 |
59 | private func handleRefresh(_ task: BGAppRefreshTask) {
60 | task.expirationHandler = {
61 | task.setTaskCompleted(success: false)
62 | }
63 | scheduleRefresh()
64 | sync() {
65 | NotificationsManager.shared.scheduleRefresh()
66 | task.setTaskCompleted(success: true)
67 | }
68 | }
69 |
70 | func invalidate() {
71 | timer?.invalidate()
72 | }
73 |
74 | func scheduleSync() {
75 | timer?.invalidate()
76 | timer = Timer.scheduledTimer(
77 | withTimeInterval: 5*60,
78 | repeats: true
79 | ) { [weak self] _ in
80 | self?.sync()
81 | }
82 | }
83 |
84 | func sync(withCompletionHandler completionHandler: @escaping () -> Void = { }) {
85 | do {
86 | if let path = try FolderSyncDefaults.load() {
87 | logger.debug("Initiating sync to \(path).")
88 | Backend.shared.createDatabaseCopy().onComplete { [weak self] tempPath in
89 | guard let self else { return }
90 | guard let tempURL = URL(string: tempPath) else { return }
91 | if path.startAccessingSecurityScopedResource() {
92 | defer {
93 | path.stopAccessingSecurityScopedResource()
94 | }
95 |
96 | self.performMerge(from: path)
97 | self.performSave(to: path, from: tempURL)
98 | } else {
99 | logger.error("Failed to acquire security access to \(path).")
100 | }
101 | completionHandler()
102 | }
103 | } else {
104 | logger.debug("No sync folder. Doing nothing.")
105 | completionHandler()
106 | }
107 | } catch {
108 | logger.error("Failed to load sync folder: \(error)")
109 | completionHandler()
110 | }
111 | }
112 |
113 | private func performMerge(from root: URL) {
114 | do {
115 | if let latestDB = latestDatabaseFile(from: root), !latestDB.absoluteString.contains(id) {
116 | let manager = FileManager.default
117 | let destPath = try manager.url(
118 | for: .itemReplacementDirectory,
119 | in: .userDomainMask,
120 | appropriateFor: root,
121 | create: true
122 | ).appendingPathComponent("\(id).sqlite3")
123 |
124 | try manager.copyItem(at: latestDB, to: destPath)
125 | try Backend.shared.mergeDatabaseCopy(atPath: destPath.absoluteString).wait()
126 | }
127 | } catch {
128 | logger.error("Failed to perform database merge; \(error)")
129 | }
130 | }
131 |
132 | private func performSave(to root: URL, from tempPath: URL) {
133 | do {
134 | let manager = FileManager.default
135 | let destURL = root.appendingPathComponent("\(id).sqlite3")
136 | let sourceURL = URL(fileURLWithPath: tempPath.absoluteString)
137 | if manager.fileExists(atPath: destURL.relativePath) {
138 | try manager.removeItem(at: destURL)
139 | }
140 |
141 | try manager.moveItem(at: sourceURL, to: destURL)
142 | } catch {
143 | logger.error("Failed to save database to sync folder: \(error)")
144 | }
145 | }
146 |
147 | private func latestDatabaseFile(from root: URL) -> URL? {
148 | do {
149 | let manager = FileManager.default
150 | let entries = try manager.contentsOfDirectory(atPath: root.relativePath)
151 | var latestEntry: URL?
152 | for entry in entries {
153 | if entry.suffix(8) == ".sqlite3" {
154 | if let latest = latestEntry {
155 | let latestAttributes = try manager.attributesOfItem(atPath: latest.relativePath)
156 | let latestModifiedAt = latestAttributes[.modificationDate] as! Date
157 | let entryURL = root.appendingPathComponent(entry)
158 | let entryAttributes = try manager.attributesOfItem(atPath: entryURL.relativePath)
159 | let entryModifiedAt = entryAttributes[.modificationDate] as! Date
160 |
161 | if entryModifiedAt > latestModifiedAt {
162 | latestEntry = entryURL
163 | }
164 | } else {
165 | latestEntry = root.appendingPathComponent(entry)
166 | }
167 | }
168 | }
169 |
170 | return latestEntry
171 | } catch {
172 | logger.error("Failed to find latest database file: \(error)")
173 | return nil
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/remember-ios/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BGTaskSchedulerPermittedIdentifiers
6 |
7 | io.defn.remember.NotificationsManager.refresh
8 | io.defn.remember.FolderSyncer.refresh
9 |
10 | ITSAppUsesNonExemptEncryption
11 |
12 | NSUserActivityTypes
13 |
14 | ReminderIntent
15 |
16 | UIBackgroundModes
17 |
18 | fetch
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/remember-ios/NotificationsManager.swift:
--------------------------------------------------------------------------------
1 | import BackgroundTasks
2 | import Foundation
3 | import NoiseSerde
4 | import UserNotifications
5 | import os
6 |
7 | fileprivate let logger = Logger(
8 | subsystem: "io.defn.remember-ios",
9 | category: "NotificationsManager"
10 | )
11 |
12 | class NotificationsManager: NSObject {
13 | static let shared = NotificationsManager()
14 |
15 | private var entries = [String: Entry]()
16 |
17 | private static let refreshIdentifier = "io.defn.remember.NotificationsManager.refresh"
18 |
19 | override init() {
20 | super.init()
21 |
22 | let center = UNUserNotificationCenter.current()
23 | center.delegate = self
24 | center.requestAuthorization(options: [.alert, .badge, .sound]) { _, error in
25 | guard error == nil else {
26 | logger.error("Did not receive authorization to send notifications: \(error)")
27 | return
28 | }
29 | }
30 | }
31 |
32 | func registerTasks() {
33 | logger.debug("Registering refresh task.")
34 | BGTaskScheduler.shared.register(
35 | forTaskWithIdentifier: Self.refreshIdentifier,
36 | using: nil) { [weak self] task in
37 | self?.handleRefresh(task as! BGAppRefreshTask)
38 | }
39 | }
40 |
41 | func scheduleRefresh() {
42 | logger.debug("Preparing to schedule refresh.")
43 | Backend.shared.getPendingEntries().onComplete { entries in
44 | let now = UVarint(Date().timeIntervalSince1970)
45 | let request = BGAppRefreshTaskRequest(identifier: Self.refreshIdentifier)
46 | var deadline: TimeInterval? = nil
47 | for entry in entries {
48 | if let dueAt = entry.dueAt, dueAt >= now {
49 | deadline = TimeInterval(dueAt)
50 | break
51 | }
52 | }
53 | guard let deadline = deadline else {
54 | logger.warning("No pending tasks, not scheduling refresh.")
55 | return
56 | }
57 | request.earliestBeginDate = Date(timeIntervalSince1970: deadline)
58 | logger.debug("Scheduling refresh at \(request.earliestBeginDate!.ISO8601Format()).")
59 |
60 | do {
61 | try BGTaskScheduler.shared.submit(request)
62 | } catch {
63 | logger.error("Failed to schedule refresh: \(error)")
64 | }
65 | }
66 | }
67 |
68 | func unscheduleRefresh() {
69 | BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: Self.refreshIdentifier)
70 | }
71 |
72 | private func handleRefresh(_ task: BGAppRefreshTask) {
73 | scheduleRefresh()
74 |
75 | let future = Backend.shared.getDueEntries()
76 | future.onComplete { entries in
77 | RunLoop.main.schedule {
78 | self.notify(ofEntries: entries)
79 | task.setTaskCompleted(success: true)
80 | }
81 | }
82 |
83 | task.expirationHandler = {
84 | future.cancel()
85 | task.setTaskCompleted(success: false)
86 | }
87 | }
88 |
89 | func removePendingNotification(for entry: Entry) {
90 | assert(Thread.current.isMainThread)
91 | entries.removeValue(forKey: entry.notificationId)
92 | let center = UNUserNotificationCenter.current()
93 | center.removePendingNotificationRequests(withIdentifiers: [entry.notificationId])
94 | center.setBadgeCount(entries.count)
95 | }
96 |
97 | func notify(ofEntries entries: [Entry]) {
98 | assert(Thread.current.isMainThread)
99 | let center = UNUserNotificationCenter.current()
100 | center.removePendingNotificationRequests(withIdentifiers: self.entries.map { $1.notificationId })
101 | center.setBadgeCount(0)
102 | self.entries.removeAll(keepingCapacity: true)
103 | for entry in entries {
104 | self.entries[entry.notificationId] = entry
105 | let content = UNMutableNotificationContent()
106 | content.title = "Remember"
107 | content.body = entry.title
108 | content.badge = NSNumber(value: entries.count)
109 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 15, repeats: false)
110 | let request = UNNotificationRequest(
111 | identifier: entry.notificationId,
112 | content: content,
113 | trigger: trigger)
114 | UNUserNotificationCenter
115 | .current()
116 | .add(request) { _ in }
117 | }
118 | center.setBadgeCount(entries.count)
119 | }
120 | }
121 |
122 | extension NotificationsManager: UNUserNotificationCenterDelegate {
123 | func userNotificationCenter(
124 | _ center: UNUserNotificationCenter,
125 | didReceive response: UNNotificationResponse,
126 | withCompletionHandler completionHandler: @escaping () -> Void) {
127 |
128 | RunLoop.main.schedule { [weak self] in
129 | guard let self else { return }
130 | if let entry = self.entries[response.notification.request.identifier] {
131 | self.removePendingNotification(for: entry)
132 | }
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/remember-ios/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/remember-ios/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryUserDefaults
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | CA92.1
13 |
14 |
15 |
16 | NSPrivacyAccessedAPIType
17 | NSPrivacyAccessedAPICategoryFileTimestamp
18 | NSPrivacyAccessedAPITypeReasons
19 |
20 | 3B52.1
21 | C617.1
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/remember-ios/Remember.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import UIKit
3 |
4 | @main
5 | struct Remember: App {
6 | private let notifications = NotificationsManager.shared
7 | private let syncer = FolderSyncer.shared
8 |
9 | init() {
10 | NotificationsManager.shared.registerTasks()
11 | FolderSyncer.shared.registerTasks()
12 | }
13 |
14 | var body: some Scene {
15 | WindowGroup {
16 | ContentView()
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/remember-ios/ReminderDetailView.swift:
--------------------------------------------------------------------------------
1 | import NoiseSerde
2 | import SwiftUI
3 |
4 | struct ReminderDetailView: View {
5 | @FocusState private var focused
6 | @State var command: String
7 | let action: (String) -> Void
8 |
9 | var body: some View {
10 | NavigationView {
11 | VStack(alignment: .leading) {
12 | CommandField($command)
13 | .focused($focused)
14 | .frame(width: nil, height: 48)
15 | Spacer()
16 | }
17 | .padding()
18 | }
19 | .toolbar {
20 | ToolbarItem {
21 | Button("Done") {
22 | action(command)
23 | }
24 | }
25 | }
26 | .onAppear {
27 | focused = true
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/remember-ios/RemindersView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct RemindersView: View {
4 | @ObservedObject var store: Store
5 |
6 | @State private var loaded = false
7 | @State private var path = [Entry]()
8 |
9 | var body: some View {
10 | NavigationStack(path: $path) {
11 | if !loaded {
12 | ProgressView()
13 | .padding()
14 | }
15 | List {
16 | ForEach(store.entries) { entry in
17 | NavigationLink(value: entry) {
18 | Reminder(
19 | store: store,
20 | entry: entry
21 | )
22 | }
23 | }
24 | }
25 | .listStyle(.plain)
26 | .navigationTitle("Remember")
27 | .navigationDestination(for: Entry.self) { entry in
28 | ReminderDetailView(command: entry.title) { command in
29 | store.update(entry: entry, withCommand: command) {
30 | RunLoop.main.schedule {
31 | path = []
32 | }
33 | }
34 | }
35 | .navigationTitle("Edit Reminder")
36 | .navigationBarTitleDisplayMode(.inline)
37 | }
38 | .refreshable {
39 | FolderSyncer.shared.sync {
40 | store.loadEntries()
41 | }
42 | }
43 | .onShake { _ in
44 | _ = Backend.shared.undo()
45 | }
46 | .onAppear {
47 | if !loaded {
48 | Backend.shared.ping().onComplete { _ in
49 | RunLoop.main.schedule {
50 | loaded = true
51 | }
52 | }
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
59 | fileprivate struct Reminder: View {
60 | @ObservedObject var store: Store
61 | let entry: Entry
62 |
63 | var body: some View {
64 | HStack {
65 | Text(entry.title)
66 | Spacer()
67 | if let dueIn = entry.dueIn {
68 | Text(dueIn)
69 | .font(.footnote)
70 | .foregroundStyle(.secondary)
71 | }
72 | }
73 | .swipeActions(edge: .leading) {
74 | Button(action: {
75 | store.snooze(entry: entry)
76 | }, label: {
77 | Label("Snooze", systemImage: "bell.slash.fill")
78 | }).tint(.secondary)
79 | }
80 | .swipeActions(edge: .trailing) {
81 | Button(action: {
82 | store.archive(entry: entry)
83 | }, label: {
84 | Label("Archive", systemImage: "checkmark.circle")
85 | }).tint(.accentColor)
86 | Button(role: .destructive, action: {
87 | store.delete(entry: entry)
88 | }, label: {
89 | Label("Delete", systemImage: "trash.slash.fill")
90 | }).tint(.red)
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/remember-ios/SettingsSyncView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import os
3 |
4 | fileprivate let logger = Logger(
5 | subsystem: "io.defn.remember-ios",
6 | category: "SettingsSyncView"
7 | )
8 |
9 | struct SettingsSyncView: View {
10 | @State var sync = (try? FolderSyncDefaults.load()) != nil
11 | @State var presentFileImporter = false
12 |
13 | var body: some View {
14 | List {
15 | Section {
16 | Toggle(isOn: $sync, label: {
17 | Text("Sync")
18 | }).disabled(!sync)
19 | Button {
20 | presentFileImporter.toggle()
21 | } label: {
22 | Text("Select Folder...")
23 | }.fileImporter(
24 | isPresented: $presentFileImporter,
25 | allowedContentTypes: [.folder]
26 | ) { result in
27 | switch result {
28 | case .failure(_):
29 | FolderSyncDefaults.clear()
30 | sync = false
31 | case .success(let url):
32 | do {
33 | try FolderSyncDefaults.save(path: url)
34 | sync = true
35 | } catch {
36 | logger.error("Failed to save sync folder defaults: \(error)")
37 | }
38 | }
39 | }
40 | }
41 | }
42 | .navigationTitle("Sync")
43 | .navigationBarTitleDisplayMode(.inline)
44 | }
45 | }
46 |
47 |
48 | class FolderSyncDefaults {
49 | private static let KEY = "sync.folder"
50 |
51 | static func load() throws -> URL? {
52 | return try UserDefaults.standard.data(forKey: KEY).flatMap { d in
53 | var isStale = false
54 | let url = try URL(
55 | resolvingBookmarkData: d,
56 | bookmarkDataIsStale: &isStale)
57 | if isStale {
58 | return nil
59 | }
60 |
61 | return url
62 | }
63 | }
64 |
65 | static func save(path: URL) throws {
66 | guard path.startAccessingSecurityScopedResource() else {
67 | logger.error("Failed to access security scoped resource.")
68 | return
69 | }
70 | defer { path.stopAccessingSecurityScopedResource() }
71 | let bookmark = try path.bookmarkData(
72 | options: [.minimalBookmark],
73 | includingResourceValuesForKeys: nil,
74 | relativeTo: nil)
75 | UserDefaults.standard.setValue(bookmark, forKey: KEY)
76 | }
77 |
78 | static func clear() {
79 | UserDefaults.standard.removeObject(forKey: KEY)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/remember-ios/SettingsView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SettingsView: View {
4 | var body: some View {
5 | NavigationView {
6 | List {
7 | Section(content: {
8 | NavigationLink("Sync") {
9 | SettingsSyncView()
10 | }
11 | }, header: {
12 | Text("General")
13 | })
14 |
15 | Section(content: {
16 | Button(action: {
17 | UIApplication.shared.open(URL(string: "https://remember.defn.io/manual/")!)
18 | }, label: {
19 | Text("Manual")
20 | })
21 | HStack {
22 | Text("Version")
23 | Spacer()
24 | Text("1.0.0")
25 | }
26 | }, header: {
27 | Text("About")
28 | })
29 | }
30 | .navigationTitle("Settings")
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/remember-ios/Store.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 | import UserNotifications
4 | import os
5 |
6 | fileprivate let logger = Logger(
7 | subsystem: "io.defn.remember-ios",
8 | category: "Store"
9 | )
10 |
11 | class Store: ObservableObject {
12 | @Published var entries = [Entry]()
13 |
14 | private var timer: Timer?
15 |
16 | init() {
17 | loadEntries()
18 |
19 | Backend.shared.installCallback(entriesDidChangeCb: { [weak self] _ in
20 | logger.debug("Entries changed.")
21 | self?.loadEntries()
22 | }).onComplete {
23 | _ = Backend.shared.markReadyForChanges()
24 | }
25 |
26 | Backend.shared.installCallback(entriesDueCb: { [weak self] entries in
27 | logger.debug("Have \(entries.count) due entries.")
28 | RunLoop.main.schedule {
29 | self?.loadEntries()
30 | NotificationsManager.shared.notify(ofEntries: entries)
31 | }
32 | }).onComplete {
33 | _ = Backend.shared.startScheduler()
34 | }
35 | }
36 |
37 | func archive(entry: Entry) {
38 | Backend.shared.archive(entryWithId: entry.id).onComplete {
39 | NotificationsManager.shared.removePendingNotification(for: entry)
40 | }
41 | }
42 |
43 | func delete(entry: Entry) {
44 | Backend.shared.delete(entryWithId: entry.id).onComplete {
45 | NotificationsManager.shared.removePendingNotification(for: entry)
46 | }
47 | }
48 |
49 | func snooze(entry: Entry) {
50 | Backend.shared.snooze(entryWithId: entry.id, forMinutes: 15).onComplete {
51 | NotificationsManager.shared.removePendingNotification(for: entry)
52 | }
53 | }
54 |
55 | func update(
56 | entry: Entry,
57 | withCommand command: String,
58 | andCompletionHandler completionHandler: @escaping () -> Void = { }
59 | ) {
60 | Backend.shared.update(entryWithId: entry.id, andCommand: command).onComplete { _ in
61 | NotificationsManager.shared.removePendingNotification(for: entry)
62 | completionHandler()
63 | }
64 | }
65 |
66 | func invalidate() {
67 | assert(Thread.current.isMainThread)
68 | logger.debug("Invalidating timer.")
69 | timer?.invalidate()
70 | }
71 |
72 | func scheduleLoadEntries() {
73 | assert(Thread.current.isMainThread)
74 | logger.debug("Scheduling entry load timer.")
75 | timer?.invalidate()
76 | timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
77 | logger.debug("Entry load timer fired.")
78 | self?.loadEntries()
79 | }
80 | }
81 |
82 | func loadEntries() {
83 | logger.debug("Loading entries.")
84 | Backend.shared.getPendingEntries().onComplete { [weak self] entries in
85 | RunLoop.main.schedule {
86 | self?.entries = entries
87 | Backend.shared.getDueEntries().onComplete { entries in
88 | NotificationsManager.shared.notify(ofEntries: entries)
89 | }
90 | }
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/remember/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // remember
4 | //
5 | // Created by Bogdan Popa on 23/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import Combine
11 | import SwiftUI
12 | import UserNotifications
13 | import os
14 |
15 | @NSApplicationMain
16 | class AppDelegate: NSObject, NSApplicationDelegate {
17 | var window: NSWindow!
18 |
19 | private var statusItem: NSStatusItem?
20 |
21 | private var syncer: FolderSyncer!
22 |
23 | func applicationDidFinishLaunching(_ aNotification: Notification) {
24 | syncer = FolderSyncer()
25 | syncer.start()
26 |
27 | let contentView = ContentView()
28 | let hostingView = NSHostingView(rootView: contentView)
29 |
30 | window = NSWindow(
31 | contentRect: NSRect(x: 0, y: 0, width: 680, height: 0),
32 | styleMask: [.titled, .fullSizeContentView],
33 | backing: .buffered,
34 | defer: false
35 | )
36 | window.collectionBehavior = .moveToActiveSpace
37 | window.backgroundColor = .clear
38 | window.isMovableByWindowBackground = true
39 | window.isOpaque = false
40 | window.contentView = hostingView
41 | window.titleVisibility = .hidden
42 | window.titlebarAppearsTransparent = true
43 | window.styleMask = [.titled, .fullSizeContentView]
44 |
45 | setupStatusItem()
46 | setupHotKey()
47 | setupHidingListener()
48 | setupUserNotifications()
49 |
50 | // This serves the same purpose as `windowDidBecomeKey` in `WindowDelegate`.
51 | if !NSApp.isActive {
52 | NSApp.activate(ignoringOtherApps: true)
53 | }
54 |
55 | OnboardingManager.shared.show()
56 | }
57 |
58 | func applicationWillBecomeActive(_ notification: Notification) {
59 | positionWindow()
60 | window.makeKeyAndOrderFront(nil)
61 | }
62 |
63 | func applicationWillResignActive(_ notification: Notification) {
64 | NSApp.hide(nil)
65 |
66 | // Re-position the window in case it was moved around by the user. Re-positioning it
67 | // now prevents it from moving around when the user re-activates the application later.
68 | positionWindow()
69 | }
70 |
71 | func applicationWillTerminate(_ aNotification: Notification) {
72 | UserNotificationsManager.shared.dismissAll()
73 | }
74 |
75 | /// Ensures that the window is always positioned in exactly the same spot. Roughly the same position as Spotlight.
76 | private func positionWindow() {
77 | if let screenFrame = NSScreen.main?.visibleFrame {
78 | let screenWidth = screenFrame.size.width
79 | let screenHeight = screenFrame.size.height
80 |
81 | let x = (screenWidth - window.frame.size.width) / 2
82 | let y = (screenHeight * 0.80) - window.frame.size.height
83 | let f = NSRect(x: x, y: y, width: window.frame.size.width, height: window.frame.size.height)
84 | .offsetBy(dx: screenFrame.origin.x, dy: screenFrame.origin.y)
85 |
86 | window.setFrame(f, display: true)
87 | }
88 | }
89 |
90 | private func setupStatusItem() {
91 | if StatusItemDefaults.shouldShow() {
92 | showStatusItem()
93 | }
94 |
95 | Notifications.observeDidToggleStatusItem { show in
96 | if show {
97 | StatusItemDefaults.show()
98 | self.showStatusItem()
99 | } else {
100 | StatusItemDefaults.hide()
101 | self.hideStatusItem()
102 | }
103 | }
104 | }
105 |
106 | private func showStatusItem() {
107 | statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
108 | if let button = statusItem?.button {
109 | let icon = NSImage(named: NSImage.Name("StatusBarIcon"))
110 | icon?.isTemplate = true
111 | button.image = icon
112 | button.action = nil
113 | }
114 |
115 | let menu = NSMenu()
116 | menu.addItem(NSMenuItem(title: "Show Remember", action: #selector(showApplicationFromStatusItem(_:)), keyEquivalent: ""))
117 | menu.addItem(NSMenuItem.separator())
118 | menu.addItem(NSMenuItem(title: "Help...", action: #selector(showHelpFromStatusItem(_:)), keyEquivalent: ""))
119 | menu.addItem(NSMenuItem(title: "Manual...", action: #selector(showManualFromStatusItem(_:)), keyEquivalent: ""))
120 | menu.addItem(NSMenuItem.separator())
121 | menu.addItem(NSMenuItem(title: "Preferences...", action: #selector(showPreferencesFromStatusItem(_:)), keyEquivalent: ","))
122 | menu.addItem(NSMenuItem.separator())
123 | menu.addItem(NSMenuItem(title: "Quit Remember", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
124 | statusItem?.menu = menu
125 | }
126 |
127 | private func hideStatusItem() {
128 | if let item = statusItem {
129 | NSStatusBar.system.removeStatusItem(item)
130 | }
131 | }
132 |
133 | @objc private func showApplicationFromStatusItem(_ sender: Any) {
134 | NSApp.activate(ignoringOtherApps: true)
135 | }
136 |
137 | @objc private func showHelpFromStatusItem(_ sender: Any) {
138 | NSApp.activate(ignoringOtherApps: true)
139 | showHelp(sender)
140 | }
141 |
142 | @objc private func showManualFromStatusItem(_ sender: Any) {
143 | if let url = Bundle.main.url(forResource: "res/manual/index", withExtension: "html") {
144 | NSWorkspace.shared.open(url)
145 | }
146 | }
147 |
148 | @objc private func showPreferencesFromStatusItem(_ sender: Any) {
149 | NSApp.activate(ignoringOtherApps: true)
150 | showPreferences(sender)
151 | }
152 |
153 | private func setupHotKey() {
154 | KeyboardShortcut.register()
155 | }
156 |
157 | /// Sets up the global hiding listener. This is triggered whenever the user intends to hide the window.
158 | private func setupHidingListener() {
159 | Notifications.observeWillHideWindow {
160 | NSApp.hide(nil)
161 | }
162 | }
163 |
164 | /// Sets up access to the notification center and installs an async notification listener to handle `entries-due` events.
165 | private func setupUserNotifications() {
166 | UserNotificationsManager.shared.setup()
167 | }
168 |
169 | /// Called whenever the user presses ⌘,
170 | @IBAction func showPreferences(_ sender: Any) {
171 | PreferencesManager.shared.show()
172 | }
173 |
174 | /// Called whenever the user presses ⌘?
175 | @IBAction func showHelp(_ sender: Any) {
176 | OnboardingManager.shared.show(force: true)
177 | }
178 |
179 | /// Called whenever the global hot key changes.
180 | @objc func didChangeHotKey(_ sender: DDHotKeyTextField) {
181 | KeyboardShortcutDefaults(fromHotKey: sender.hotKey).save()
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-1024.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-128.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-16.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-256.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-32.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-512.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/AppIcon.appiconset/1024x1024@3x-64.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "1024x1024@3x-16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "1024x1024@3x-32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "1024x1024@3x-32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "1024x1024@3x-64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "1024x1024@3x-128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "1024x1024@3x-256.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "1024x1024@3x-256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "1024x1024@3x-512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "1024x1024@3x-512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "1024x1024@3x-1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/remember/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/remember/Assets.xcassets/Icon.imageset/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/Icon.imageset/32x32.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/Icon.imageset/32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/Icon.imageset/32x32@2x.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/Icon.imageset/32x32@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/Icon.imageset/32x32@3x.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/Icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "32x32.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "32x32@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "32x32@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/remember/Assets.xcassets/OnboardingStep1-1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "OnboardingStep1-1.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/remember/Assets.xcassets/OnboardingStep1-1.imageset/OnboardingStep1-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/OnboardingStep1-1.imageset/OnboardingStep1-1.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/OnboardingStep1-2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "OnboardingStep1-2.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/remember/Assets.xcassets/OnboardingStep1-2.imageset/OnboardingStep1-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/OnboardingStep1-2.imageset/OnboardingStep1-2.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/OnboardingStep1-3.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "OnboardingStep1-3.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/remember/Assets.xcassets/OnboardingStep1-3.imageset/OnboardingStep1-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/OnboardingStep1-3.imageset/OnboardingStep1-3.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/OnboardingStep2-1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "OnboardingStep2-1.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/remember/Assets.xcassets/OnboardingStep2-1.imageset/OnboardingStep2-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/OnboardingStep2-1.imageset/OnboardingStep2-1.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/OnboardingStep2-2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "OnboardingStep2-2.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/remember/Assets.xcassets/OnboardingStep2-2.imageset/OnboardingStep2-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/OnboardingStep2-2.imageset/OnboardingStep2-2.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/StatusBarIcon.imageset/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/StatusBarIcon.imageset/16x16.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/StatusBarIcon.imageset/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/StatusBarIcon.imageset/32x32.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/StatusBarIcon.imageset/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/remember/Assets.xcassets/StatusBarIcon.imageset/64x64.png
--------------------------------------------------------------------------------
/remember/Assets.xcassets/StatusBarIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "16x16.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "32x32.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "64x64.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/remember/BackendExtensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | #if arch(arm64)
5 | let ARCH = "arm64"
6 | #else
7 | let ARCH = "x86_64"
8 | #endif
9 |
10 | // - MARK: Backend
11 | extension Backend {
12 | static let shared = Backend(
13 | withZo: Bundle.main.url(forResource: "res/core-\(ARCH)", withExtension: "zo")!,
14 | andMod: "main",
15 | andProc: "main"
16 | )
17 | }
18 |
19 | // - MARK: Entry
20 | extension Entry: Identifiable {
21 | }
22 |
23 | // - MARK: Token
24 | extension Token {
25 | var color: Color {
26 | switch self.data {
27 | case .relativeTime(_, _):
28 | return .blue
29 | case .namedDatetime(_):
30 | return .blue
31 | case .namedDate(_):
32 | return .blue
33 | case .recurrence(_, _):
34 | return .green
35 | case .tag(_):
36 | return .red
37 | default:
38 | return .primary
39 | }
40 | }
41 |
42 | var range: NSRange {
43 | return NSRange(
44 | location: Int(span.lo.offset),
45 | length: Int(span.hi.offset - span.lo.offset))
46 | }
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/remember/BridgingHeader.h:
--------------------------------------------------------------------------------
1 | //
2 | // DDHotKey-Bridging-Headder.h
3 | // remember
4 | //
5 | // Created by Bogdan Popa on 27/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | #ifndef DDHotKey_Bridging_Headder_h
10 | #define DDHotKey_Bridging_Headder_h
11 |
12 | #import "vendor/DDHotKey/DDHotKeyCenter.h"
13 | #import "vendor/DDHotKey/DDHotKeyTextField.h"
14 | #import "vendor/DDHotKey/DDHotKeyUtilities.h"
15 |
16 | #endif /* DDHotKey_Bridging_Headder_h */
17 |
--------------------------------------------------------------------------------
/remember/CommandField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomTextField.swift
3 | // remember
4 | //
5 | // Created by Bogdan Popa on 23/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | enum CommandAction {
13 | case update(String)
14 | case cancel(String)
15 | case commit(String)
16 | case archive
17 | case delete
18 | case previous
19 | case next
20 | case undo
21 | }
22 |
23 | struct CommandField: NSViewRepresentable {
24 | typealias NSViewType = NSTextField
25 |
26 | @Binding var text: String
27 |
28 | private let action: (CommandAction) -> Void
29 |
30 | init(_ text: Binding,
31 | action theAction: @escaping (CommandAction) -> Void) {
32 | _text = text
33 | action = theAction
34 | }
35 |
36 | func makeNSView(context: NSViewRepresentableContext) -> NSViewType {
37 | let field = CommandTextField()
38 | field.allowsEditingTextAttributes = true
39 | field.backgroundColor = NSColor.clear
40 | field.delegate = context.coordinator
41 | field.font = .systemFont(ofSize: 24)
42 | field.isBordered = false
43 | field.focusRingType = .none
44 | field.placeholderString = "Remember"
45 |
46 | field.keyBindings.append(contentsOf: [
47 | KeyBinding(withKeyCode: Keycode.z, andModifierFlags: [.command], using: #selector(Coordinator.undo(_:))),
48 | ])
49 |
50 | return field
51 | }
52 |
53 | func updateNSView(_ nsView: NSViewType, context: NSViewRepresentableContext) {
54 | nsView.stringValue = text
55 | }
56 |
57 | func makeCoordinator() -> Coordinator {
58 | return Coordinator(action: {
59 | self.action($0)
60 | }, setter: {
61 | self.text = $0
62 | })
63 | }
64 |
65 | final class Coordinator: NSObject, NSTextFieldDelegate {
66 | private var action: (CommandAction) -> Void
67 | private var setter: (String) -> Void
68 | private var timer: Timer?
69 |
70 | var didBecomeFirstResponder = false
71 |
72 | init(action: @escaping (CommandAction) -> Void,
73 | setter: @escaping (String) -> Void) {
74 |
75 | self.action = action
76 | self.setter = setter
77 | }
78 |
79 | func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
80 | if commandSelector == #selector(NSResponder.insertNewline(_:)) {
81 | action(.commit(control.stringValue))
82 | return true
83 | } else if commandSelector == #selector(NSResponder.cancelOperation(_:)) {
84 | action(.cancel(control.stringValue))
85 | return true
86 | } else if commandSelector == #selector(NSResponder.moveUp(_:)) || commandSelector == #selector(NSResponder.insertBacktab(_:)) {
87 | action(.previous)
88 | return true
89 | } else if commandSelector == #selector(NSResponder.moveDown(_:)) || commandSelector == #selector(NSResponder.insertTab(_:)) {
90 | action(.next)
91 | return true
92 | } else if commandSelector == #selector(NSResponder.deleteBackward(_:)) && control.stringValue.isEmpty {
93 | action(.archive)
94 | return true
95 | } else if commandSelector == #selector(NSResponder.deleteWordBackward(_:)) && control.stringValue.isEmpty {
96 | action(.delete)
97 | return true
98 | }
99 |
100 | return false
101 | }
102 |
103 | @objc func undo(_ sender: NSTextField) {
104 | if sender.stringValue.isEmpty {
105 | action(.undo)
106 | }
107 | }
108 |
109 | func scheduleHighlight(ofTextField field: NSTextField) {
110 | timer?.invalidate()
111 | timer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: false) { [weak self] _ in
112 | self?.highlight(textField: field)
113 | }
114 | }
115 |
116 | func highlight(textField field: NSTextField) {
117 | guard !field.stringValue.isEmpty else { return }
118 | guard let tokens = try? Backend.shared.parse(command: field.stringValue).wait() else { return }
119 | let systemFont = NSFont.systemFont(ofSize: 24)
120 | let attributedText = NSMutableAttributedString(string: field.stringValue)
121 | attributedText.beginEditing()
122 | attributedText.setAttributes(
123 | [NSAttributedString.Key.font: systemFont],
124 | range: NSRange(location: 0, length: attributedText.length))
125 | for token in tokens {
126 | attributedText.addAttribute(
127 | .foregroundColor,
128 | value: NSColor(token.color),
129 | range: token.range)
130 | }
131 | attributedText.endEditing()
132 | field.attributedStringValue = attributedText
133 | }
134 |
135 | func controlTextDidChange(_ aNotification: Notification) {
136 | if let textField = aNotification.object as? NSTextField {
137 | setter(textField.stringValue)
138 | action(.update(textField.stringValue))
139 | scheduleHighlight(ofTextField: textField)
140 | }
141 | }
142 | }
143 | }
144 |
145 | fileprivate struct KeyBinding {
146 | private let keyCode: UInt16
147 | private let modifierFlags: NSEvent.ModifierFlags
148 |
149 | let selector: Selector
150 |
151 | init(withKeyCode keyCode: UInt16, andModifierFlags modifierFlags: NSEvent.ModifierFlags, using selector: Selector) {
152 | self.keyCode = keyCode
153 | self.modifierFlags = modifierFlags
154 | self.selector = selector
155 | }
156 |
157 | func matches(_ event: NSEvent) -> Bool {
158 | return event.keyCode == keyCode &&
159 | event.modifierFlags.contains(modifierFlags)
160 | }
161 | }
162 |
163 | fileprivate class CommandTextField: NSTextField {
164 | var keyBindings = [KeyBinding]()
165 |
166 | override func performKeyEquivalent(with event: NSEvent) -> Bool {
167 | for binding in keyBindings {
168 | if binding.matches(event) {
169 | return NSApp.sendAction(binding.selector, to: delegate, from: self)
170 | }
171 | }
172 |
173 | return super.performKeyEquivalent(with: event)
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/remember/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // remember
4 | //
5 | // Created by Bogdan Popa on 23/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import SwiftUI
11 |
12 | struct ContentView: View {
13 | @ObservedObject private var store = Store()
14 |
15 | var body: some View {
16 | VStack(alignment: .leading, spacing: nil) {
17 | HStack {
18 | Image("Icon")
19 | .resizable()
20 | .frame(width: 32, height: 32, alignment: .leading)
21 |
22 | CommandField($store.command) {
23 | switch $0 {
24 | case .update(_):
25 | self.store.hideEntries()
26 | case .cancel(_):
27 | self.store.clear()
28 | case .commit(let c):
29 | self.store.commit(command: c)
30 | case .archive:
31 | if self.store.entriesVisible {
32 | self.store.archiveCurrentEntry()
33 | }
34 | case .delete:
35 | if self.store.entriesVisible {
36 | self.store.deleteCurrentEntry()
37 | }
38 | case .previous:
39 | self.store.updatePendingEntries {
40 | self.store.selectPreviousEntry()
41 | }
42 | case .next:
43 | self.store.updatePendingEntries {
44 | self.store.selectNextEntry()
45 | }
46 | case .undo:
47 | self.store.undo()
48 | }
49 | }
50 | }
51 | .padding(.top, 15)
52 | .padding(.leading, 15)
53 | .padding(.trailing, 15)
54 | .padding(.bottom, entriesVisible ? 10 : 15)
55 |
56 | if entriesVisible {
57 | Divider()
58 | EntryList($store.entries, currentEntry: $store.currentEntry)
59 | }
60 | }
61 | .visualEffect()
62 | .frame(width: 680, height: nil, alignment: .leading)
63 | .fixedSize()
64 | .cornerRadius(8)
65 | }
66 |
67 | var entriesVisible: Bool {
68 | store.entriesVisible && !store.entries.isEmpty
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/remember/EntryList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EntryList.swift
3 | // Remember
4 | //
5 | // Created by Bogdan Popa on 30/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct EntryList: View {
13 | private let title = "Pending"
14 |
15 | @Binding var entries: [Entry]
16 | @Binding var currentEntry: Entry?
17 |
18 | init(_ entries: Binding<[Entry]>, currentEntry: Binding) {
19 | _entries = entries
20 | _currentEntry = currentEntry
21 | }
22 |
23 | var body: some View {
24 | VStack(alignment: .leading, spacing: 3) {
25 | HStack {
26 | Text(title.uppercased())
27 | .font(.caption)
28 | .foregroundColor(.secondary)
29 | Spacer()
30 | Text("\(entries.count)")
31 | .font(.caption)
32 | .foregroundColor(.secondary)
33 | .padding(.leading, 5)
34 | .padding(.trailing, 5)
35 | .overlay(
36 | Capsule(style: .continuous)
37 | .stroke(Color.secondary, lineWidth: 1)
38 | )
39 | }
40 | .padding(.top, 5)
41 | .padding(.bottom, 5)
42 | .padding(.leading, 10)
43 | .padding(.trailing, 10)
44 | .opacity(0.75)
45 |
46 | ForEach(visibleEntries()) { entry in
47 | EntryListItem(entry, isCurrent: self.currentEntry.map { entry.id == $0.id } ?? false)
48 | }
49 | }
50 | }
51 |
52 | func visibleEntries() -> [Entry] {
53 | guard let currentEntry = self.currentEntry else {
54 | return []
55 | }
56 |
57 | if let index = entries.firstIndex(where: { $0.id == currentEntry.id }) {
58 | if index - 2 < 0 {
59 | let lo = 0
60 | let hi = min(5, entries.count)
61 | return Array(entries[lo ..< hi])
62 | } else if index + 2 >= entries.count {
63 | let lo = max(0, entries.count - 5)
64 | let hi = entries.count
65 | return Array(entries[lo ..< hi])
66 | } else {
67 | let lo = index - 2
68 | let hi = index + 2
69 | return Array(entries[lo ... hi])
70 | }
71 | }
72 |
73 | return []
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/remember/EntryListItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EntryListEntry.swift
3 | // Remember
4 | //
5 | // Created by Bogdan Popa on 30/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct EntryListItem: View {
13 | let entry: Entry
14 | let isCurrent: Bool
15 |
16 | init(_ entry: Entry, isCurrent: Bool) {
17 | self.entry = entry
18 | self.isCurrent = isCurrent
19 | }
20 |
21 | var body: some View {
22 | HStack {
23 | Text(entry.title)
24 | Spacer()
25 | if entry.recurs {
26 | Image(nsImage: NSImage(named: NSImage.refreshFreestandingTemplateName)!)
27 | .foregroundColor(isCurrent ? Color.white : Color.secondary)
28 | }
29 | dueIn
30 | }
31 | .frame(width: nil, height: 32, alignment: .center)
32 | .padding(.leading, 10)
33 | .padding(.trailing, 10)
34 | .background(isCurrent ? Color.accentColor : .clear)
35 | .foregroundColor(isCurrent ? Color(NSColor.white) : .primary)
36 | }
37 |
38 | var dueIn: some View {
39 | entry.dueIn.map { text in
40 | Text(text)
41 | .padding(5)
42 | .font(.system(size: 10))
43 | .foregroundColor(isCurrent ? Color.white : Color.secondary)
44 | .overlay(
45 | Capsule(style: .continuous)
46 | .stroke(isCurrent ? Color.white : Color.secondary, lineWidth: 1)
47 | )
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/remember/FolderSync.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FolderSync.swift
3 | // Remember
4 | //
5 | // Created by Bogdan Popa on 24/01/2020.
6 | // Copyright © 2020-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os
11 |
12 | fileprivate let logger = Logger(
13 | subsystem: "io.defn.remember",
14 | category: "FolderSyncer"
15 | )
16 |
17 | class FolderSyncer {
18 | private var timer: Timer?
19 |
20 | func start(withFrequency frequency: TimeInterval = 5 * 60) {
21 | scheduleSync(withFrequency: frequency)
22 | sync()
23 |
24 | Notifications.observeDidRequestSync { [weak self] in
25 | guard let self else { return }
26 | self.scheduleSync(withFrequency: frequency)
27 | self.sync()
28 | }
29 | }
30 |
31 | private func scheduleSync(withFrequency frequency: TimeInterval) {
32 | timer?.invalidate()
33 | timer = Timer.scheduledTimer(
34 | withTimeInterval: frequency,
35 | repeats: true
36 | ) { [weak self] _ in
37 | self?.sync()
38 | }
39 | }
40 |
41 | private func sync() {
42 | do {
43 | if let path = try FolderSyncDefaults.load() {
44 | logger.debug("Initiating sync to \(path).")
45 | Backend.shared.createDatabaseCopy().onComplete { [weak self] tempPath in
46 | guard let self else { return }
47 | guard let tempURL = URL(string: tempPath) else { return }
48 | if path.startAccessingSecurityScopedResource() {
49 | defer {
50 | path.stopAccessingSecurityScopedResource()
51 | }
52 |
53 | self.performMerge(from: path)
54 | self.performSave(to: path, from: tempURL)
55 | } else {
56 | logger.error("Failed to acquire security access to \(path).")
57 | }
58 | }
59 | }
60 | } catch {
61 | logger.error("Failed to load sync folder: \(error)")
62 | }
63 | }
64 |
65 | private func syncId() -> String {
66 | if let id = UserDefaults.standard.string(forKey: "syncId") {
67 | return id
68 | }
69 |
70 | let id = UUID().uuidString
71 | UserDefaults.standard.set(id, forKey: "syncId")
72 | return id
73 | }
74 |
75 | private func performMerge(from root: URL) {
76 | do {
77 | if let latestDB = latestDatabaseFile(from: root), !latestDB.absoluteString.contains(syncId()) {
78 | let manager = FileManager.default
79 | let destPath = try manager.url(
80 | for: .itemReplacementDirectory,
81 | in: .userDomainMask,
82 | appropriateFor: root,
83 | create: true
84 | ).appendingPathComponent("\(syncId()).sqlite3")
85 |
86 | try manager.copyItem(at: latestDB, to: destPath)
87 | try Backend.shared.mergeDatabaseCopy(atPath: destPath.absoluteString).wait()
88 | }
89 | } catch {
90 | logger.error("Failed to perform database merge; \(error)")
91 | }
92 | }
93 |
94 | private func performSave(to root: URL, from tempPath: URL) {
95 | do {
96 | let manager = FileManager.default
97 | let destURL = root.appendingPathComponent("\(self.syncId()).sqlite3")
98 | let sourceURL = URL(fileURLWithPath: tempPath.absoluteString)
99 | if manager.fileExists(atPath: destURL.relativePath) {
100 | try manager.removeItem(at: destURL)
101 | }
102 |
103 | try manager.moveItem(at: sourceURL, to: destURL)
104 | } catch {
105 | logger.error("Failed to save database to sync folder: \(error)")
106 | }
107 | }
108 |
109 | private func latestDatabaseFile(from root: URL) -> URL? {
110 | do {
111 | let manager = FileManager.default
112 | let entries = try manager.contentsOfDirectory(atPath: root.relativePath)
113 | var latestEntry: URL?
114 | for entry in entries {
115 | if entry.suffix(8) == ".sqlite3" {
116 | if let latest = latestEntry {
117 | let latestAttributes = try manager.attributesOfItem(atPath: latest.relativePath)
118 | let latestModifiedAt = latestAttributes[.modificationDate] as! Date
119 | let entryURL = root.appendingPathComponent(entry)
120 | let entryAttributes = try manager.attributesOfItem(atPath: entryURL.relativePath)
121 | let entryModifiedAt = entryAttributes[.modificationDate] as! Date
122 |
123 | if entryModifiedAt > latestModifiedAt {
124 | latestEntry = entryURL
125 | }
126 | } else {
127 | latestEntry = root.appendingPathComponent(entry)
128 | }
129 | }
130 | }
131 |
132 | return latestEntry
133 | } catch {
134 | logger.error("Failed to find latest database file: \(error)")
135 | return nil
136 | }
137 | }
138 | }
139 |
140 | class FolderSyncDefaults {
141 | private static let KEY = "sync"
142 |
143 | static func load() throws -> URL? {
144 | return try UserDefaults.standard.data(forKey: KEY).flatMap { d in
145 | var isStale = false
146 | let url = try URL(
147 | resolvingBookmarkData: d,
148 | options: [.withSecurityScope, .withoutUI],
149 | relativeTo: nil,
150 | bookmarkDataIsStale: &isStale)
151 | if isStale {
152 | return nil
153 | }
154 |
155 | return url
156 | }
157 | }
158 |
159 | static func save(path: URL) throws {
160 | let bookmark = try path.bookmarkData(
161 | options: .withSecurityScope,
162 | includingResourceValuesForKeys: nil,
163 | relativeTo: nil)
164 | UserDefaults.standard.setValue(bookmark, forKey: KEY)
165 | }
166 |
167 | static func clear() {
168 | UserDefaults.standard.removeObject(forKey: KEY)
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/remember/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ITSAppUsesNonExemptEncryption
6 |
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIconFile
12 |
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
21 | CFBundleShortVersionString
22 | $(MARKETING_VERSION)
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSApplicationCategoryType
26 | public.app-category.productivity
27 | LSMinimumSystemVersion
28 | $(MACOSX_DEPLOYMENT_TARGET)
29 | LSUIElement
30 |
31 | NSAppTransportSecurity
32 |
33 | NSExceptionDomains
34 |
35 | local.remember
36 |
37 | NSExceptionAllowsInsecureHTTPLoads
38 |
39 |
40 |
41 |
42 | NSHumanReadableCopyright
43 | Copyright © 2024 CLEARTYPE SRL. All rights reserved.
44 | NSMainStoryboardFile
45 | Main
46 | NSPrincipalClass
47 | NSApplication
48 | NSSupportsAutomaticTermination
49 |
50 | NSSupportsSuddenTermination
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/remember/KeyboardShortcut.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardShortcut.swift
3 | // Remember
4 | //
5 | // Created by Bogdan Popa on 30/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class KeyboardShortcut {
12 | static func register() {
13 | let defaults = KeyboardShortcutDefaults.load()
14 |
15 | DDHotKeyCenter.shared()?.registerHotKey(
16 | withKeyCode: defaults.keyCode,
17 | modifierFlags: defaults.modifierFlags,
18 | task: { _ in
19 |
20 | if NSApp.isActive {
21 | Notifications.willHideWindow()
22 | } else {
23 | NSApp.activate(ignoringOtherApps: true)
24 | }
25 | })
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/remember/KeyboardShortcutDefaults.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardShortcutDefaults.swift
3 | // Remember
4 | //
5 | // Created by Bogdan Popa on 30/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct KeyboardShortcutDefaults: Codable {
12 | let keyCode: UInt16
13 | let modifierFlags: UInt
14 |
15 | init(keyCode: UInt16, modifierFlags: UInt) {
16 | self.keyCode = keyCode
17 | self.modifierFlags = modifierFlags
18 | }
19 |
20 | init(fromHotKey hk: DDHotKey) {
21 | self.keyCode = hk.keyCode
22 | self.modifierFlags = hk.modifierFlags
23 | }
24 |
25 | static func load() -> KeyboardShortcutDefaults {
26 | if let data = UserDefaults.standard.data(forKey: "keyboardShortcut") {
27 | return (try? JSONDecoder().decode(KeyboardShortcutDefaults.self, from: data)) ?? `default`()
28 | }
29 |
30 | return `default`()
31 | }
32 |
33 | func save() {
34 | if let data = try? JSONEncoder().encode(self) {
35 | UserDefaults.standard.set(data, forKey: "keyboardShortcut")
36 | }
37 | }
38 |
39 | static func `default`() -> KeyboardShortcutDefaults {
40 | KeyboardShortcutDefaults(
41 | keyCode: Keycode.space,
42 | modifierFlags: NSEvent.ModifierFlags.option.rawValue)
43 | }
44 |
45 | static func asString() -> String {
46 | let defaults = load()
47 | return DDStringFromKeyCode(defaults.keyCode, defaults.modifierFlags)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/remember/KeyboardShortcutField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardShortcutField.swift
3 | // Remember
4 | //
5 | // Created by Bogdan Popa on 30/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct KeyboardShortcutField: NSViewRepresentable {
13 | typealias NSViewType = DDHotKeyTextField
14 |
15 | func makeNSView(context: NSViewRepresentableContext) -> NSViewType {
16 | return DDHotKeyTextField()
17 | }
18 |
19 | func updateNSView(_ nsView: NSViewType, context: NSViewRepresentableContext) {
20 | let defaults = KeyboardShortcutDefaults.load()
21 |
22 | nsView.hotKey = DDHotKey(
23 | keyCode: defaults.keyCode,
24 | modifierFlags: defaults.modifierFlags,
25 | task: { _ in })
26 | nsView.target = NSApplication.shared.delegate
27 | nsView.action = #selector(AppDelegate.didChangeHotKey(_:))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/remember/Keycode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Keycode.swift
3 | // remember
4 | //
5 | // Created by Bogdan Popa on 27/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Source: https://gist.github.com/swillits/df648e87016772c7f7e5dbed2b345066
12 | struct Keycode {
13 | // Layout-independent Keys
14 | // eg.These key codes are always the same key on all layouts.
15 | static let returnKey : UInt16 = 0x24
16 | static let enter : UInt16 = 0x4C
17 | static let tab : UInt16 = 0x30
18 | static let space : UInt16 = 0x31
19 | static let delete : UInt16 = 0x33
20 | static let escape : UInt16 = 0x35
21 | static let command : UInt16 = 0x37
22 | static let shift : UInt16 = 0x38
23 | static let capsLock : UInt16 = 0x39
24 | static let option : UInt16 = 0x3A
25 | static let control : UInt16 = 0x3B
26 | static let rightShift : UInt16 = 0x3C
27 | static let rightOption : UInt16 = 0x3D
28 | static let rightControl : UInt16 = 0x3E
29 | static let leftArrow : UInt16 = 0x7B
30 | static let rightArrow : UInt16 = 0x7C
31 | static let downArrow : UInt16 = 0x7D
32 | static let upArrow : UInt16 = 0x7E
33 | static let volumeUp : UInt16 = 0x48
34 | static let volumeDown : UInt16 = 0x49
35 | static let mute : UInt16 = 0x4A
36 | static let help : UInt16 = 0x72
37 | static let home : UInt16 = 0x73
38 | static let pageUp : UInt16 = 0x74
39 | static let forwardDelete : UInt16 = 0x75
40 | static let end : UInt16 = 0x77
41 | static let pageDown : UInt16 = 0x79
42 | static let function : UInt16 = 0x3F
43 | static let f1 : UInt16 = 0x7A
44 | static let f2 : UInt16 = 0x78
45 | static let f4 : UInt16 = 0x76
46 | static let f5 : UInt16 = 0x60
47 | static let f6 : UInt16 = 0x61
48 | static let f7 : UInt16 = 0x62
49 | static let f3 : UInt16 = 0x63
50 | static let f8 : UInt16 = 0x64
51 | static let f9 : UInt16 = 0x65
52 | static let f10 : UInt16 = 0x6D
53 | static let f11 : UInt16 = 0x67
54 | static let f12 : UInt16 = 0x6F
55 | static let f13 : UInt16 = 0x69
56 | static let f14 : UInt16 = 0x6B
57 | static let f15 : UInt16 = 0x71
58 | static let f16 : UInt16 = 0x6A
59 | static let f17 : UInt16 = 0x40
60 | static let f18 : UInt16 = 0x4F
61 | static let f19 : UInt16 = 0x50
62 | static let f20 : UInt16 = 0x5A
63 |
64 | // US-ANSI Keyboard Positions
65 | // eg. These key codes are for the physical key (in any keyboard layout)
66 | // at the location of the named key in the US-ANSI layout.
67 | static let a : UInt16 = 0x00
68 | static let b : UInt16 = 0x0B
69 | static let c : UInt16 = 0x08
70 | static let d : UInt16 = 0x02
71 | static let e : UInt16 = 0x0E
72 | static let f : UInt16 = 0x03
73 | static let g : UInt16 = 0x05
74 | static let h : UInt16 = 0x04
75 | static let i : UInt16 = 0x22
76 | static let j : UInt16 = 0x26
77 | static let k : UInt16 = 0x28
78 | static let l : UInt16 = 0x25
79 | static let m : UInt16 = 0x2E
80 | static let n : UInt16 = 0x2D
81 | static let o : UInt16 = 0x1F
82 | static let p : UInt16 = 0x23
83 | static let q : UInt16 = 0x0C
84 | static let r : UInt16 = 0x0F
85 | static let s : UInt16 = 0x01
86 | static let t : UInt16 = 0x11
87 | static let u : UInt16 = 0x20
88 | static let v : UInt16 = 0x09
89 | static let w : UInt16 = 0x0D
90 | static let x : UInt16 = 0x07
91 | static let y : UInt16 = 0x10
92 | static let z : UInt16 = 0x06
93 |
94 | static let zero : UInt16 = 0x1D
95 | static let one : UInt16 = 0x12
96 | static let two : UInt16 = 0x13
97 | static let three : UInt16 = 0x14
98 | static let four : UInt16 = 0x15
99 | static let five : UInt16 = 0x17
100 | static let six : UInt16 = 0x16
101 | static let seven : UInt16 = 0x1A
102 | static let eight : UInt16 = 0x1C
103 | static let nine : UInt16 = 0x19
104 |
105 | static let equals : UInt16 = 0x18
106 | static let minus : UInt16 = 0x1B
107 | static let semicolon : UInt16 = 0x29
108 | static let apostrophe : UInt16 = 0x27
109 | static let comma : UInt16 = 0x2B
110 | static let period : UInt16 = 0x2F
111 | static let forwardSlash : UInt16 = 0x2C
112 | static let backslash : UInt16 = 0x2A
113 | static let grave : UInt16 = 0x32
114 | static let leftBracket : UInt16 = 0x21
115 | static let rightBracket : UInt16 = 0x1E
116 |
117 | static let keypadDecimal : UInt16 = 0x41
118 | static let keypadMultiply : UInt16 = 0x43
119 | static let keypadPlus : UInt16 = 0x45
120 | static let keypadClear : UInt16 = 0x47
121 | static let keypadDivide : UInt16 = 0x4B
122 | static let keypadEnter : UInt16 = 0x4C
123 | static let keypadMinus : UInt16 = 0x4E
124 | static let keypadEquals : UInt16 = 0x51
125 | static let keypad0 : UInt16 = 0x52
126 | static let keypad1 : UInt16 = 0x53
127 | static let keypad2 : UInt16 = 0x54
128 | static let keypad3 : UInt16 = 0x55
129 | static let keypad4 : UInt16 = 0x56
130 | static let keypad5 : UInt16 = 0x57
131 | static let keypad6 : UInt16 = 0x58
132 | static let keypad7 : UInt16 = 0x59
133 | static let keypad8 : UInt16 = 0x5B
134 | static let keypad9 : UInt16 = 0x5C
135 | }
136 |
--------------------------------------------------------------------------------
/remember/Notifications.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Notifications.swift
3 | // remember
4 | //
5 | // Created by Bogdan Popa on 27/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import NoiseSerde
11 |
12 | struct Notifications {
13 | private static func observe(_ name: NSNotification.Name, using handler: @escaping (Notification) -> Void) {
14 | NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil, using: handler)
15 | }
16 |
17 | static func didToggleStatusItem(show: Bool) {
18 | NotificationCenter.default.post(
19 | name: .didToggleStatusItem,
20 | object: show)
21 | }
22 |
23 | static func observeDidToggleStatusItem(withCompletionHandler handler: @escaping (Bool) -> Void) {
24 | observe(.didToggleStatusItem) { notification in
25 | if let show = notification.object as? Bool {
26 | handler(show)
27 | }
28 | }
29 | }
30 |
31 | static func didRequestSync() {
32 | NotificationCenter.default.post(
33 | name: .didRequestSync,
34 | object: nil)
35 | }
36 |
37 | static func observeDidRequestSync(withCompletionHandler handler: @escaping () -> Void) {
38 | observe(.didRequestSync) { notification in
39 | handler()
40 | }
41 | }
42 |
43 | static func willHideWindow() {
44 | NotificationCenter.default.post(
45 | name: .willHideWindow,
46 | object: nil)
47 | }
48 |
49 | static func observeWillHideWindow(withCompletionHandler handler: @escaping () -> Void) {
50 | observe(.willHideWindow) { _ in
51 | handler()
52 | }
53 | }
54 |
55 | static func willArchiveEntry(entryId: UVarint) {
56 | NotificationCenter.default.post(
57 | name: .willArchiveEntry,
58 | object: entryId)
59 | }
60 |
61 | static func observeWillArchiveEntry(withCompletionHandler handler: @escaping (UVarint) -> Void) {
62 | observe(.willArchiveEntry) { notification in
63 | if let id = notification.object as? UVarint {
64 | handler(id)
65 | }
66 | }
67 | }
68 |
69 | static func willSelectEntry(entryId: UVarint) {
70 | NotificationCenter.default.post(
71 | name: .willSelectEntry,
72 | object: entryId)
73 | }
74 |
75 | static func observeWillSelectEntry(withCompletionHandler handler: @escaping (UVarint) -> Void) {
76 | observe(.willSelectEntry) { notification in
77 | if let id = notification.object as? UVarint {
78 | handler(id)
79 | }
80 | }
81 | }
82 |
83 | static func willSnoozeEntry(entryId: UVarint) {
84 | NotificationCenter.default.post(
85 | name: .willSnoozeEntry,
86 | object: entryId)
87 | }
88 |
89 | static func observeWillSnoozeEntry(withCompletionHandler handler: @escaping (UVarint) -> Void) {
90 | observe(.willSnoozeEntry) { notification in
91 | if let id = notification.object as? UVarint {
92 | handler(id)
93 | }
94 | }
95 | }
96 | }
97 |
98 | extension Notification.Name {
99 | static let didToggleStatusItem = Notification.Name("io.defn.remember.didToggleStatusItem")
100 | static let didRequestSync = Notification.Name("io.defn.remember.didRequestSync")
101 | static let willHideWindow = Notification.Name("io.defn.remember.willHideWindow")
102 | static let willArchiveEntry = Notification.Name("io.defn.remember.willArchiveEntry")
103 | static let willSelectEntry = Notification.Name("io.defn.remember.willSelectEntry")
104 | static let willSnoozeEntry = Notification.Name("io.defn.remember.willSnoozeEntry")
105 | }
106 |
--------------------------------------------------------------------------------
/remember/Onboarding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Onboarding.swift
3 | // Remember
4 | //
5 | // Created by Bogdan Popa on 21/01/2020.
6 | // Copyright © 2020-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | class OnboardingManager: NSObject, NSWindowDelegate {
13 | static let shared = OnboardingManager()
14 |
15 | private var window: OnboardingWindow!
16 |
17 | private override init() {
18 | super.init()
19 |
20 | let window = OnboardingWindow(
21 | contentRect: NSRect(x: 0, y: 0, width: 600, height: 600),
22 | styleMask: [.titled],
23 | backing: .buffered,
24 | defer: false)
25 | window.delegate = self
26 | window.title = "Welcome to Remember"
27 |
28 | self.window = window
29 | }
30 |
31 | func show(force: Bool = false) {
32 | if (force || !onboardingWasSeen()) {
33 | self.window.contentView = NSHostingView(rootView: OnboardingView(store: OnboardingStore()))
34 | self.window.center()
35 | self.window.makeKeyAndOrderFront(self)
36 | }
37 | }
38 |
39 | func hide() {
40 | markOnboardingSeen()
41 | window.orderOut(self)
42 | window.contentView = nil
43 | }
44 |
45 | func windowShouldClose(_ sender: NSWindow) -> Bool {
46 | hide()
47 | return false
48 | }
49 | }
50 |
51 | fileprivate enum Step {
52 | case one
53 | case two
54 | }
55 |
56 | fileprivate class OnboardingStore: ObservableObject {
57 | @Published var currentStep = Step.one
58 |
59 | func `continue`() {
60 | switch currentStep {
61 | case .one:
62 | currentStep = .two
63 | case .two:
64 | OnboardingManager.shared.hide()
65 | }
66 | }
67 | }
68 |
69 | fileprivate struct StepFrame: View where Content: View {
70 | @Environment(\.colorScheme) var colorScheme
71 |
72 | private let content: () -> Content
73 |
74 | init(_ content: @escaping () -> Content) {
75 | self.content = content
76 | }
77 |
78 | var body: some View {
79 | VStack {
80 | content()
81 | Spacer()
82 | }
83 | .padding(25)
84 | .frame(width: 600, height: nil, alignment: .top)
85 | .background(colorScheme == .dark ? Color.black : Color.white)
86 | .clipped()
87 | .shadow(radius: 2)
88 | }
89 | }
90 |
91 | fileprivate struct Pill: View where Content: View {
92 | @Environment(\.colorScheme) var colorScheme
93 |
94 | let content: () -> Content
95 |
96 | init(@ViewBuilder content: @escaping () -> Content) {
97 | self.content = content
98 | }
99 |
100 | var body: some View {
101 | VStack {
102 | content()
103 | }
104 | .padding(10)
105 | .background(colorScheme == .dark ? Color.gray.opacity(0.3) : Color.gray.opacity(0.05))
106 | .cornerRadius(5)
107 | }
108 | }
109 |
110 | fileprivate struct OnboardingStep1: View {
111 | var body: some View {
112 | StepFrame {
113 | VStack {
114 | Text("Welcome to Remember")
115 | .font(.largeTitle)
116 | Text("Stash distractions away for later.")
117 | .font(.subheadline)
118 | .foregroundColor(.secondary)
119 |
120 | Spacer()
121 |
122 | VStack(alignment: .center, spacing: 10) {
123 | Pill {
124 | Text("Remember is a keyboard-driven application. To activate or de-activate it, you can press ") +
125 | Text(KeyboardShortcutDefaults.asString()).bold() +
126 | Text(" at any time.")
127 | }
128 |
129 | Pill {
130 | HStack {
131 | Text("When the application is active, you can add entries like ") +
132 | Text("buy milk").bold() +
133 | Text(" and press ") +
134 | Text("return").bold() +
135 | Text(" to save them.")
136 |
137 | Image("OnboardingStep1-1")
138 | .resizable()
139 | .frame(width: 258, height: 64, alignment: .center)
140 | }
141 | }
142 |
143 | Pill {
144 | HStack {
145 | Image("OnboardingStep1-2")
146 | .resizable()
147 | .frame(width: 258, height: 64, alignment: .center)
148 | Text("Entries can contain modifiers like ") +
149 | Text("+1d").foregroundColor(.accentColor) +
150 | Text(" or ") +
151 | Text("@10am").foregroundColor(.accentColor) +
152 | Text(" that tell Remember when it should remind you about them.")
153 | }
154 | }
155 |
156 | Pill {
157 | HStack {
158 | Text("You can use the arrow keys to navigate through your pending entries and ") +
159 | Text("⌫").bold() +
160 | Text(" to archive any of the ones you're done with.")
161 |
162 | Image("OnboardingStep1-3")
163 | .resizable()
164 | .frame(width: 335, height: 128, alignment: .center)
165 | }
166 | }
167 | }
168 | .padding(.top, 10)
169 | .padding(.bottom, 10)
170 | }
171 | }
172 | }
173 | }
174 |
175 | fileprivate struct OnboardingStep2: View {
176 | var body: some View {
177 | StepFrame {
178 | VStack {
179 | Spacer()
180 |
181 | VStack(alignment: .center, spacing: 10) {
182 | Pill {
183 | OnboardingStep2P1()
184 | }
185 |
186 | Pill {
187 | Text("Press ") +
188 | Text("⌘,").bold() +
189 | Text(" to bring up the Preferences window where you can change the default key binding and make Remember launch at login.")
190 | }
191 |
192 | Pill {
193 | Text("Press ") +
194 | Text("⌘/").bold() +
195 | Text(" to go through this guide again whenever you need a refresher on how Remember works.")
196 | }
197 |
198 | Pill {
199 | (
200 | Text("Want to learn more? ") +
201 | Text("Read the user manual...")
202 | .foregroundColor(Color.accentColor)
203 | )
204 | .onTapGesture(perform: {
205 | if let url = Bundle.main.url(forResource: "manual/index", withExtension: "html") {
206 | NSWorkspace.shared.open(url)
207 | }
208 | })
209 | }
210 | }
211 |
212 | Spacer()
213 | }
214 | }
215 | }
216 | }
217 |
218 | // Extracted because Swift can't typecheck it on Xcode 12.5.
219 | fileprivate struct OnboardingStep2P1: View {
220 | var body: some View {
221 | VStack {
222 | HStack {
223 | text
224 |
225 | Image("OnboardingStep2-1")
226 | .resizable()
227 | .frame(width: 258.5, height: 64, alignment: .center)
228 | }
229 |
230 | Image("OnboardingStep2-2")
231 | .resizable()
232 | .frame(width: 301, height: 128, alignment: .center)
233 | }
234 | }
235 |
236 | var text: some View {
237 | (
238 | Text("You can create repeating entries by using modifiers like ") +
239 | Text("\\*daily\\*").foregroundColor(.green) +
240 | Text(", ") +
241 | Text("\\*weekly\\*").foregroundColor(.green) +
242 | Text(" or ") +
243 | Text("\\*every two days\\*").foregroundColor(.green) +
244 | Text(". These entries update their due date whenever you archive them.")
245 | )
246 | .frame(width: nil, height: 88, alignment: .leading)
247 | .lineLimit(nil)
248 | }
249 | }
250 |
251 | fileprivate struct OnboardingView: View {
252 | @ObservedObject var store: OnboardingStore
253 |
254 | var body: some View {
255 | VStack {
256 | if store.currentStep == .one {
257 | OnboardingStep1()
258 | } else if store.currentStep == .two {
259 | OnboardingStep2()
260 | }
261 |
262 | HStack(alignment: .center, spacing: nil) {
263 | Button(action: {
264 | self.store.continue()
265 | }, label: {
266 | Text(store.currentStep == .one ? "Continue" : "Get Started")
267 | .padding(.leading, 20)
268 | .padding(.trailing, 20)
269 | })
270 | }
271 | .frame(width: nil, height: 48, alignment: .center)
272 | }
273 | .frame(width: 600, height: 600, alignment: .top)
274 | }
275 | }
276 |
277 | fileprivate class OnboardingWindow: NSWindow {}
278 |
279 | fileprivate func onboardingWasSeen() -> Bool {
280 | return UserDefaults.standard.bool(forKey: "onboardingSeen")
281 | }
282 |
283 | fileprivate func markOnboardingSeen() {
284 | UserDefaults.standard.set(true, forKey: "onboardingSeen")
285 | }
286 |
--------------------------------------------------------------------------------
/remember/Preferences.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Preferences.swift
3 | // Remember
4 | //
5 | // Created by Bogdan Popa on 30/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import Foundation
11 | import LaunchAtLogin
12 | import SwiftUI
13 |
14 | class PreferencesManager: NSObject, NSWindowDelegate {
15 | static let shared = PreferencesManager()
16 |
17 | private var window: PreferencesWindow!
18 | private var toolbarDelegate: PreferencesWindowToolbarDelegate!
19 |
20 | private override init() {
21 | super.init()
22 |
23 | let window = PreferencesWindow(
24 | contentRect: NSRect(x: 0, y: 0, width: 500, height: 300),
25 | styleMask: [.closable, .titled],
26 | backing: .buffered,
27 | defer: false)
28 | window.delegate = self
29 | window.title = "General"
30 |
31 | let toolbar = NSToolbar()
32 | toolbarDelegate = PreferencesWindowToolbarDelegate()
33 | toolbar.delegate = toolbarDelegate
34 | toolbar.selectedItemIdentifier = .general
35 | window.toolbar = toolbar
36 | if #available(macOS 11, *) {
37 | window.toolbarStyle = .preference
38 | }
39 |
40 | self.window = window
41 | }
42 |
43 | func show() {
44 | self.window.contentView = NSHostingView(rootView: GeneralPreferencesView())
45 | self.window.center()
46 | self.window.makeKeyAndOrderFront(self)
47 | }
48 |
49 | func windowShouldClose(_ sender: NSWindow) -> Bool {
50 | KeyboardShortcut.register()
51 | self.window.orderOut(self)
52 | self.window.contentView = nil
53 | return false
54 | }
55 | }
56 |
57 | private struct GeneralPreferencesView : View {
58 | @ObservedObject var store = PreferencesStore()
59 |
60 | let formatter: NumberFormatter = {
61 | let fmt = NumberFormatter()
62 | fmt.minimum = 1
63 | fmt.maximum = 1440
64 | return fmt
65 | }()
66 |
67 | var body: some View {
68 | Form {
69 | Section {
70 | VStack {
71 | Preference("Startup:") {
72 | Toggle("Launch Remember at Login", isOn: $store.launchAtLogin)
73 | }
74 | Preference("Behavior:") {
75 | Toggle("Show Menu Bar Icon", isOn: $store.showStatusIcon)
76 | }
77 | Preference("Snooze minutes:") {
78 | TextField("", value: $store.snoozeMinutes, formatter: formatter)
79 | .frame(width: 300, height: nil, alignment: .leading)
80 | .offset(x: -8, y: 0)
81 | }
82 | Preference("Show Remember:") {
83 | KeyboardShortcutField()
84 | }
85 | .padding([.top, .bottom], 10)
86 | Preference("Sync:") {
87 | VStack(alignment: .leading, spacing: nil) {
88 | if let url = self.store.syncFolder {
89 | Text(url.relativePath)
90 | .foregroundColor(.secondary)
91 | }
92 |
93 | HStack {
94 | Button(action: {
95 | let panel = NSOpenPanel()
96 | panel.prompt = "Set Sync Folder"
97 | panel.allowsMultipleSelection = false
98 | panel.canChooseFiles = false
99 | panel.canChooseDirectories = true
100 | panel.canCreateDirectories = true
101 |
102 | if panel.runModal() == .OK {
103 | self.store.syncFolder = panel.urls[0]
104 | }
105 | }, label: {
106 | Text("Set Sync Folder...")
107 | })
108 |
109 | if self.store.syncFolder != nil {
110 | Button(action: {
111 | self.store.syncFolder = nil
112 | }, label: {
113 | Text("Stop Syncing")
114 | })
115 | Button(action: {
116 | Notifications.didRequestSync()
117 | }, label: {
118 | Text("Sync")
119 | })
120 | }
121 | }
122 | }
123 | }
124 | }
125 | }
126 | }
127 | .padding(15)
128 | }
129 | }
130 |
131 | private class PreferencesStore: NSObject, ObservableObject {
132 | @Published var launchAtLogin = LaunchAtLogin.isEnabled
133 | @Published var showStatusIcon = StatusItemDefaults.shouldShow()
134 | @Published var snoozeMinutes = SnoozeDefaults.get()
135 | @Published var syncFolder = try! FolderSyncDefaults.load()
136 |
137 | private var launchAtLoginCancellable: AnyCancellable?
138 | private var showStatusIconCancellable: AnyCancellable?
139 | private var snoozeMinutesCancelable: AnyCancellable?
140 | private var syncFolderCancellable: AnyCancellable?
141 |
142 | override init() {
143 | super.init()
144 |
145 | launchAtLoginCancellable = $launchAtLogin.sink {
146 | LaunchAtLogin.isEnabled = $0
147 | }
148 |
149 | showStatusIconCancellable = $showStatusIcon.sink {
150 | Notifications.didToggleStatusItem(show: $0)
151 | }
152 |
153 | snoozeMinutesCancelable = $snoozeMinutes.sink {
154 | try! SnoozeDefaults.set($0)
155 | }
156 |
157 | syncFolderCancellable = $syncFolder.sink {
158 | if let path = $0 {
159 | if path.startAccessingSecurityScopedResource() {
160 | defer {
161 | path.stopAccessingSecurityScopedResource()
162 | }
163 |
164 | try! FolderSyncDefaults.save(path: path)
165 | }
166 | } else {
167 | FolderSyncDefaults.clear()
168 | }
169 | }
170 | }
171 | }
172 |
173 | private struct Preference : View {
174 | private let label: String
175 | private let content: Content
176 |
177 | init(_ label: String, @ViewBuilder content: () -> Content) {
178 | self.label = label
179 | self.content = content()
180 | }
181 |
182 | var body: some View {
183 | HStack(alignment: .top, spacing: nil) {
184 | Text(label)
185 | .frame(width: 150, height: nil, alignment: .trailing)
186 | content
187 | }
188 | .frame(width: 450, height: nil, alignment: .leading)
189 | }
190 | }
191 |
192 | private class PreferencesWindow: NSWindow {
193 |
194 | }
195 |
196 | private class PreferencesWindowToolbarDelegate: NSObject, NSToolbarDelegate {
197 | func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
198 | return [.general]
199 | }
200 |
201 | func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
202 | return [.general]
203 | }
204 |
205 | func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
206 | return [.general]
207 | }
208 |
209 | func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
210 | switch itemIdentifier {
211 | case .general:
212 | let item = NSToolbarItem(itemIdentifier: .general)
213 | item.target = self
214 | item.action = #selector(viewSelected(_:))
215 | item.isEnabled = true
216 | item.image = NSImage(named: NSImage.preferencesGeneralName)
217 | item.label = "General"
218 | return item
219 | default:
220 | return nil
221 | }
222 | }
223 |
224 | @objc func viewSelected(_ sender: NSToolbarItem) {
225 | }
226 | }
227 |
228 | private extension NSToolbarItem.Identifier {
229 | static let general = NSToolbarItem.Identifier(rawValue: "General")
230 | }
231 |
--------------------------------------------------------------------------------
/remember/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/remember/Snooze.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Snooze.swift
3 | // Remember
4 | //
5 | // Created by Bogdan Popa on 10.07.2021.
6 | // Copyright © 2021, 2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum SnoozeError: Error {
12 | case invalidMinutes
13 | }
14 |
15 | class SnoozeDefaults {
16 | private static let KEY = "snoozeMinutes"
17 | private static let DEFAULT = 45
18 |
19 | static func get() -> Int {
20 | let minutes = UserDefaults.standard.integer(forKey: KEY)
21 | if minutes == 0 {
22 | return DEFAULT
23 | }
24 | return minutes
25 | }
26 |
27 | static func set(_ minutes: Int) throws {
28 | if (minutes <= 0) {
29 | throw SnoozeError.invalidMinutes
30 | }
31 | UserDefaults.standard.setValue(minutes, forKey: KEY)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/remember/StatusItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatusItem.swift
3 | // Remember
4 | //
5 | // Created by Bogdan Popa on 24/01/2020.
6 | // Copyright © 2020-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class StatusItemDefaults {
12 | private static let KEY = "hideStatusItem"
13 |
14 | static func shouldShow() -> Bool {
15 | return !UserDefaults.standard.bool(forKey: KEY)
16 | }
17 |
18 | static func hide() {
19 | UserDefaults.standard.set(true, forKey: KEY)
20 | }
21 |
22 | static func show() {
23 | UserDefaults.standard.set(false, forKey: KEY)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/remember/Store.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommandStore.swift
3 | // remember
4 | //
5 | // Created by Bogdan Popa on 27/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import NoiseSerde
11 |
12 | class Store: ObservableObject {
13 | @Published var command = ""
14 | @Published var entries = [Entry]()
15 | @Published var entriesVisible = false
16 | @Published var currentEntry: Entry? = nil
17 | @Published var editingEntryWithId: UVarint? = nil
18 |
19 | init() {
20 | self.updatePendingEntries()
21 | try! Backend.shared.installCallback(entriesDidChangeCb: { [weak self] _ in
22 | DispatchQueue.main.async {
23 | self?.updatePendingEntries()
24 | }
25 | }).wait()
26 | try! Backend.shared.markReadyForChanges().wait()
27 |
28 | Notifications.observeWillArchiveEntry {
29 | _ = Backend.shared.archive(entryWithId: $0)
30 | }
31 |
32 | Notifications.observeWillSelectEntry { id in
33 | if let entry = self.entries.first(where: { $0.id == id }) {
34 | RunLoop.main.schedule {
35 | self.currentEntry = entry
36 | self.updatePendingEntries {
37 | self.showEntries()
38 | }
39 | }
40 | }
41 | }
42 |
43 | Notifications.observeWillSnoozeEntry {
44 | _ = Backend.shared.snooze(entryWithId: $0, forMinutes: UVarint(SnoozeDefaults.get()))
45 | }
46 | }
47 |
48 | func clear() {
49 | hideEntries()
50 | if command.isEmpty {
51 | Notifications.willHideWindow()
52 | }
53 |
54 | command = ""
55 | editingEntryWithId = nil
56 | }
57 |
58 | func commit(command: String) {
59 | commit(command: command) {
60 | Notifications.willHideWindow()
61 | }
62 | }
63 |
64 | func commit(command: String, withCompletionHandler handler: @escaping () -> Void) {
65 | if command.isEmpty {
66 | editCurrentEntry()
67 | } else if let id = editingEntryWithId {
68 | Backend.shared.update(
69 | entryWithId: id,
70 | andCommand: command
71 | ).onComplete { [weak self] _ in
72 | self?.clear()
73 | handler()
74 | }
75 | } else {
76 | Backend.shared.commit(command: command).onComplete { [weak self] _ in
77 | self?.clear()
78 | handler()
79 | }
80 | }
81 | }
82 |
83 | func hideEntries() {
84 | entriesVisible = false
85 | }
86 |
87 | func showEntries() {
88 | entriesVisible = true
89 | }
90 |
91 | func updatePendingEntries() {
92 | updatePendingEntries { }
93 | }
94 |
95 | func updatePendingEntries(withCompletionHandler handler: @escaping () -> Void) {
96 | // Ensure that the "cursor" is preserved as much as possible when the entries
97 | // change by keeping track of the current position.
98 | var currentEntryIndex = 0
99 | if let currentEntry = self.currentEntry {
100 | currentEntryIndex = entries.firstIndex(where: { $0.id == currentEntry.id }) ?? 0
101 | }
102 |
103 | Backend.shared.getPendingEntries().onComplete { [weak self] entries in
104 | guard let self else { return }
105 |
106 | self.entries = entries
107 | if entries.isEmpty {
108 | self.currentEntry = nil
109 | } else if self.currentEntry == nil {
110 | self.currentEntry = entries[0]
111 | } else if let currentEntry = self.currentEntry {
112 | if !entries.contains(where: { $0.id == currentEntry.id }) {
113 | self.currentEntry = entries[currentEntryIndex % entries.count]
114 | }
115 | }
116 |
117 | handler()
118 | }
119 | }
120 |
121 | func archiveCurrentEntry() {
122 | if let currentEntry = self.currentEntry {
123 | Backend.shared.archive(entryWithId: currentEntry.id).onComplete { [weak self] in
124 | UserNotificationsManager.shared.dismiss(byEntryId: currentEntry.id)
125 | self?.updatePendingEntries()
126 | }
127 | }
128 | }
129 |
130 | func deleteCurrentEntry() {
131 | if let currentEntry = self.currentEntry {
132 | Backend.shared.delete(entryWithId: currentEntry.id).onComplete { [weak self] in
133 | UserNotificationsManager.shared.dismiss(byEntryId: currentEntry.id)
134 | self?.updatePendingEntries()
135 | }
136 | }
137 | }
138 |
139 | func editCurrentEntry() {
140 | if let currentEntry = self.currentEntry {
141 | command = currentEntry.title
142 | editingEntryWithId = currentEntry.id
143 | }
144 | }
145 |
146 | func stopEditing() {
147 | if editingEntryWithId != nil {
148 | command = ""
149 | editingEntryWithId = nil
150 | }
151 | }
152 |
153 | private func findPreviousEntryIndex() -> Int {
154 | if let currentEntry = self.currentEntry,
155 | let index = entries.firstIndex(where: { $0.id == currentEntry.id }) {
156 |
157 | return (index - 1) < 0 ? entries.count - 1 : index - 1
158 | }
159 |
160 | return 0
161 | }
162 |
163 | private func findNextEntryIndex() -> Int {
164 | if let currentEntry = self.currentEntry,
165 | let index = entries.firstIndex(where: { $0.id == currentEntry.id }) {
166 |
167 | return (index + 1) % entries.count
168 | }
169 |
170 | return 0
171 | }
172 |
173 | func selectPreviousEntry() {
174 | if entries.isEmpty {
175 | entriesVisible = false
176 | } else if !entriesVisible {
177 | entriesVisible = true
178 | } else {
179 | currentEntry = entries[self.findPreviousEntryIndex()]
180 | stopEditing()
181 | }
182 | }
183 |
184 | func selectNextEntry() {
185 | if entries.isEmpty {
186 | entriesVisible = false
187 | } else if !entriesVisible {
188 | entriesVisible = true
189 | } else {
190 | currentEntry = entries[findNextEntryIndex()]
191 | stopEditing()
192 | }
193 | }
194 |
195 | func undo() {
196 | Backend.shared.undo().onComplete { [weak self] in
197 | self?.updatePendingEntries()
198 | }
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/remember/UserNotifications.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserNotifications.swift
3 | // Remember
4 | //
5 | // Created by Bogdan Popa on 28/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import NoiseSerde
11 | import UserNotifications
12 | import os
13 |
14 | fileprivate let logger = Logger(
15 | subsystem: "io.defn.remember",
16 | category: "UserNotifications"
17 | )
18 |
19 | enum UserNotificationInfo: String {
20 | case entryId
21 | }
22 |
23 | enum UserNotificationAction: String {
24 | case `default` = "com.apple.UNNotificationDefaultActionIdentifier"
25 | case dismiss = "com.apple.UNNotificationDismissActionIdentifier"
26 | case archive = "io.defn.remember.ArchiveAction"
27 | case snooze = "io.defn.remember.SnoozeAction"
28 | }
29 |
30 | enum UserNotificationCategory: String {
31 | case entry = "io.defn.remember.EntryCategory"
32 | }
33 |
34 | class UserNotificationsManager: NSObject, UNUserNotificationCenterDelegate {
35 | public static let shared = UserNotificationsManager()
36 |
37 | private let queue = DispatchQueue(label: "io.defn.remember.UserNotificationsManager")
38 | private var pending = [UVarint]()
39 |
40 | private override init() {
41 | super.init()
42 | }
43 |
44 | private func addPending(byId id: UVarint, withDeadline deadline: DispatchTime) -> Bool {
45 | queue.sync {
46 | if self.pending.contains(id) {
47 | return false
48 | }
49 |
50 | queue.asyncAfter(deadline: deadline) {
51 | self.pending.removeAll(where: { $0 == id })
52 | }
53 |
54 | self.pending.append(id)
55 | return true
56 | }
57 | }
58 |
59 | private func removePending(byId id: UVarint) {
60 | queue.sync {
61 | self.pending.removeAll(where: { $0 == id })
62 | }
63 | }
64 |
65 | func setup() {
66 | let center = UNUserNotificationCenter.current()
67 | center.requestAuthorization(options: [.alert, .badge, .sound], completionHandler: { granted, err in
68 | if !granted {
69 | logger.error("Alert access not granted.")
70 | return
71 | }
72 |
73 | let archiveAction = UNNotificationAction(
74 | identifier: UserNotificationAction.archive.rawValue,
75 | title: "Archive",
76 | options: [.destructive, .authenticationRequired])
77 |
78 | let snoozeAction = UNNotificationAction(
79 | identifier: UserNotificationAction.snooze.rawValue,
80 | title: "Snooze",
81 | options: [.destructive, .authenticationRequired])
82 |
83 | let entryCategory = UNNotificationCategory(
84 | identifier: UserNotificationCategory.entry.rawValue,
85 | actions: [archiveAction, snoozeAction],
86 | intentIdentifiers: [],
87 | options: .customDismissAction)
88 |
89 | center.setNotificationCategories([entryCategory])
90 | center.delegate = self
91 |
92 | try! Backend.shared.installCallback(entriesDueCb: { entries in
93 | for entry in entries {
94 | if !self.addPending(byId: entry.id, withDeadline: .now() + .seconds(15 * 60)) {
95 | logger.debug("Notification for entry \(entry.id) ignored.")
96 | continue
97 | }
98 |
99 | let content = UNMutableNotificationContent()
100 | content.title = "Remember"
101 | content.subtitle = entry.title
102 | content.sound = .default
103 | content.userInfo = [UserNotificationInfo.entryId.rawValue: entry.id]
104 | content.categoryIdentifier = UserNotificationCategory.entry.rawValue
105 |
106 | let request = UNNotificationRequest(
107 | identifier: String(entry.id),
108 | content: content,
109 | trigger: nil)
110 |
111 | center.add(request) { error in
112 | if let err = error {
113 | logger.error("Failed to add notification: \(err)")
114 | }
115 | }
116 | }
117 | }).wait()
118 | try! Backend.shared.startScheduler().wait()
119 | })
120 | }
121 |
122 | func dismiss(byEntryId id: UVarint) {
123 | let center = UNUserNotificationCenter.current()
124 | center.removeDeliveredNotifications(withIdentifiers: [String(id)])
125 | self.removePending(byId: id)
126 | }
127 |
128 | func dismissAll() {
129 | let center = UNUserNotificationCenter.current()
130 | center.removeAllDeliveredNotifications()
131 | }
132 |
133 | func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
134 | return completionHandler([.list])
135 | }
136 |
137 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
138 | guard let action = UserNotificationAction(rawValue: response.actionIdentifier) else {
139 | return completionHandler()
140 | }
141 |
142 | let userInfo = response.notification.request.content.userInfo
143 | if let id = userInfo[UserNotificationInfo.entryId.rawValue] as? UVarint {
144 | self.removePending(byId: id)
145 |
146 | switch action {
147 | case .archive:
148 | Notifications.willArchiveEntry(entryId: id)
149 | case .dismiss, .snooze:
150 | Notifications.willSnoozeEntry(entryId: id)
151 | case .`default`:
152 | Notifications.willSelectEntry(entryId: id)
153 | }
154 | }
155 |
156 | completionHandler()
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/remember/VisualEffectBackground.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VisualEffectBackground.swift
3 | // remember
4 | //
5 | // Created by Bogdan Popa on 23/12/2019.
6 | // Copyright © 2019-2024 CLEARTYPE SRL. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct VisualEffectBackground: NSViewRepresentable {
13 | typealias NSViewType = NSVisualEffectView
14 |
15 | private let blendingMode: NSVisualEffectView.BlendingMode
16 | private let material: NSVisualEffectView.Material
17 | private let state: NSVisualEffectView.State
18 |
19 | fileprivate init(
20 | blendingMode: NSVisualEffectView.BlendingMode,
21 | material: NSVisualEffectView.Material,
22 | state: NSVisualEffectView.State
23 | ) {
24 | self.blendingMode = blendingMode
25 | self.material = material
26 | self.state = state
27 | }
28 |
29 | func makeNSView(context: Context) -> NSViewType {
30 | return NSVisualEffectView()
31 | }
32 |
33 | func updateNSView(_ nsView: NSViewType, context: Context) {
34 | nsView.blendingMode = blendingMode
35 | nsView.material = material
36 | nsView.state = state
37 | }
38 | }
39 |
40 | extension View {
41 | func visualEffect(
42 | blendingMode: NSVisualEffectView.BlendingMode = .behindWindow,
43 | material: NSVisualEffectView.Material = .popover,
44 | state: NSVisualEffectView.State = .active
45 | ) -> some View {
46 | background(
47 | VisualEffectBackground(
48 | blendingMode: blendingMode,
49 | material: material,
50 | state: state
51 | )
52 | )
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/remember/remember-core.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.inherit
8 |
9 | com.apple.security.cs.allow-jit
10 |
11 | com.apple.security.cs.allow-unsigned-executable-memory
12 |
13 | com.apple.security.cs.disable-executable-page-protection
14 |
15 | com.apple.security.cs.disable-library-validation
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/remember/remember.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.cs.allow-dyld-environment-variables
8 |
9 | com.apple.security.cs.allow-jit
10 |
11 | com.apple.security.cs.allow-unsigned-executable-memory
12 |
13 | com.apple.security.cs.disable-executable-page-protection
14 |
15 | com.apple.security.cs.disable-library-validation
16 |
17 | com.apple.security.files.downloads.read-write
18 |
19 | com.apple.security.files.user-selected.read-write
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/remember/vendor/DDHotKey/DDHotKeyCenter.h:
--------------------------------------------------------------------------------
1 | /*
2 | DDHotKey -- DDHotKeyCenter.h
3 |
4 | Copyright (c) Dave DeLong
5 |
6 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
7 |
8 | The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
9 | */
10 |
11 | #import
12 |
13 | //a convenient typedef for the required signature of a hotkey block callback
14 | typedef void (^DDHotKeyTask)(NSEvent*);
15 |
16 | @interface DDHotKey : NSObject
17 |
18 | // creates a new hotkey but does not register it
19 | + (instancetype)hotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task;
20 |
21 | @property (nonatomic, assign, readonly) id target;
22 | @property (nonatomic, readonly) SEL action;
23 | @property (nonatomic, strong, readonly) id object;
24 | @property (nonatomic, copy, readonly) DDHotKeyTask task;
25 |
26 | @property (nonatomic, readonly) unsigned short keyCode;
27 | @property (nonatomic, readonly) NSUInteger modifierFlags;
28 |
29 | @end
30 |
31 | #pragma mark -
32 |
33 | @interface DDHotKeyCenter : NSObject
34 |
35 | + (instancetype)sharedHotKeyCenter;
36 |
37 | /**
38 | Register a hotkey.
39 | */
40 | - (DDHotKey *)registerHotKey:(DDHotKey *)hotKey;
41 |
42 | /**
43 | Register a target/action hotkey.
44 | The modifierFlags must be a bitwise OR of NSCommandKeyMask, NSAlternateKeyMask, NSControlKeyMask, or NSShiftKeyMask;
45 | Returns the hotkey registered. If registration failed, returns nil.
46 | */
47 | - (DDHotKey *)registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags target:(id)target action:(SEL)action object:(id)object;
48 |
49 | /**
50 | Register a block callback hotkey.
51 | The modifierFlags must be a bitwise OR of NSCommandKeyMask, NSAlternateKeyMask, NSControlKeyMask, or NSShiftKeyMask;
52 | Returns the hotkey registered. If registration failed, returns nil.
53 | */
54 | - (DDHotKey *)registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task;
55 |
56 | /**
57 | See if a hotkey exists with the specified keycode and modifier flags.
58 | NOTE: this will only check among hotkeys you have explicitly registered with DDHotKeyCenter. This does not check all globally registered hotkeys.
59 | */
60 | - (BOOL)hasRegisteredHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags;
61 |
62 | /**
63 | Unregister a specific hotkey
64 | */
65 | - (void)unregisterHotKey:(DDHotKey *)hotKey;
66 |
67 | /**
68 | Unregister all hotkeys
69 | */
70 | - (void)unregisterAllHotKeys;
71 |
72 | /**
73 | Unregister all hotkeys with a specific target
74 | */
75 | - (void)unregisterHotKeysWithTarget:(id)target;
76 |
77 | /**
78 | Unregister all hotkeys with a specific target and action
79 | */
80 | - (void)unregisterHotKeysWithTarget:(id)target action:(SEL)action;
81 |
82 | /**
83 | Unregister a hotkey with a specific keycode and modifier flags
84 | */
85 | - (void)unregisterHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags;
86 |
87 | /**
88 | Returns a set of currently registered hotkeys
89 | **/
90 | - (NSSet *)registeredHotKeys;
91 |
92 | @end
93 |
94 |
--------------------------------------------------------------------------------
/remember/vendor/DDHotKey/DDHotKeyTextField.h:
--------------------------------------------------------------------------------
1 | /*
2 | DDHotKey -- DDHotKeyTextField.h
3 |
4 | Copyright (c) Dave DeLong
5 |
6 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
7 |
8 | The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
9 | */
10 |
11 | #import
12 | #import "DDHotKeyCenter.h"
13 |
14 | @interface DDHotKeyTextField : NSTextField
15 |
16 | @property (nonatomic, strong) DDHotKey *hotKey;
17 |
18 | @end
19 |
20 | @interface DDHotKeyTextFieldCell : NSTextFieldCell @end
--------------------------------------------------------------------------------
/remember/vendor/DDHotKey/DDHotKeyTextField.m:
--------------------------------------------------------------------------------
1 | /*
2 | DDHotKey -- DDHotKeyTextField.m
3 |
4 | Copyright (c) Dave DeLong
5 |
6 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
7 |
8 | The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
9 | */
10 |
11 | #import
12 |
13 | #import "DDHotKeyTextField.h"
14 | #import "DDHotKeyUtilities.h"
15 |
16 | @interface DDHotKeyTextFieldEditor : NSTextView
17 |
18 | @property (nonatomic, weak) DDHotKeyTextField *hotKeyField;
19 |
20 | @end
21 |
22 | static DDHotKeyTextFieldEditor *DDFieldEditor(void);
23 | static DDHotKeyTextFieldEditor *DDFieldEditor(void) {
24 | static DDHotKeyTextFieldEditor *editor;
25 | static dispatch_once_t onceToken;
26 | dispatch_once(&onceToken, ^{
27 | editor = [[DDHotKeyTextFieldEditor alloc] initWithFrame:NSMakeRect(0, 0, 100, 32)];
28 | [editor setFieldEditor:YES];
29 | });
30 | return editor;
31 | }
32 |
33 | @implementation DDHotKeyTextFieldCell
34 |
35 | - (NSTextView *)fieldEditorForView:(NSView *)view {
36 | if ([view isKindOfClass:[DDHotKeyTextField class]]) {
37 | DDHotKeyTextFieldEditor *editor = DDFieldEditor();
38 | editor.insertionPointColor = editor.backgroundColor;
39 | editor.hotKeyField = (DDHotKeyTextField *)view;
40 | return editor;
41 | }
42 | return nil;
43 | }
44 |
45 | @end
46 |
47 | @implementation DDHotKeyTextField
48 |
49 | + (Class)cellClass {
50 | return [DDHotKeyTextFieldCell class];
51 | }
52 |
53 | - (void)setHotKey:(DDHotKey *)hotKey {
54 | if (_hotKey != hotKey) {
55 | _hotKey = hotKey;
56 | [super setStringValue:[DDStringFromKeyCode(hotKey.keyCode, hotKey.modifierFlags) uppercaseString]];
57 | }
58 | }
59 |
60 | - (void)setStringValue:(NSString *)aString {
61 | NSLog(@"-[DDHotKeyTextField setStringValue:] is not what you want. Use -[DDHotKeyTextField setHotKey:] instead.");
62 | [super setStringValue:aString];
63 | }
64 |
65 | - (NSString *)stringValue {
66 | NSLog(@"-[DDHotKeyTextField stringValue] is not what you want. Use -[DDHotKeyTextField hotKey] instead.");
67 | return [super stringValue];
68 | }
69 |
70 | @end
71 |
72 | @implementation DDHotKeyTextFieldEditor {
73 | BOOL _hasSeenKeyDown;
74 | id _globalMonitor;
75 | DDHotKey *_originalHotKey;
76 | }
77 |
78 | - (void)setHotKeyField:(DDHotKeyTextField *)hotKeyField {
79 | _hotKeyField = hotKeyField;
80 | _originalHotKey = _hotKeyField.hotKey;
81 | }
82 |
83 | - (void)processHotkeyEvent:(NSEvent *)event {
84 | NSUInteger flags = event.modifierFlags;
85 | BOOL hasModifier = (flags & (NSCommandKeyMask | NSAlternateKeyMask | NSControlKeyMask | NSShiftKeyMask | NSFunctionKeyMask)) > 0;
86 |
87 | if (event.type == NSKeyDown) {
88 | _hasSeenKeyDown = YES;
89 | unichar character = [event.charactersIgnoringModifiers characterAtIndex:0];
90 |
91 |
92 | if (hasModifier == NO && ([[NSCharacterSet newlineCharacterSet] characterIsMember:character] || event.keyCode == kVK_Escape)) {
93 | if (event.keyCode == kVK_Escape) {
94 | self.hotKeyField.hotKey = _originalHotKey;
95 |
96 | NSString *str = DDStringFromKeyCode(_originalHotKey.keyCode, _originalHotKey.modifierFlags);
97 | self.textStorage.mutableString.string = [str uppercaseString];
98 | }
99 | [self.hotKeyField sendAction:self.hotKeyField.action to:self.hotKeyField.target];
100 | [self.window makeFirstResponder:nil];
101 | return;
102 | }
103 | }
104 |
105 | if ((event.type == NSKeyDown || (event.type == NSFlagsChanged && _hasSeenKeyDown == NO)) && hasModifier) {
106 | self.hotKeyField.hotKey = [DDHotKey hotKeyWithKeyCode:event.keyCode modifierFlags:flags task:_originalHotKey.task];
107 | NSString *str = DDStringFromKeyCode(event.keyCode, flags);
108 | [self.textStorage.mutableString setString:[str uppercaseString]];
109 | [self.hotKeyField sendAction:self.hotKeyField.action to:self.hotKeyField.target];
110 | }
111 | }
112 |
113 | - (BOOL)becomeFirstResponder {
114 | BOOL ok = [super becomeFirstResponder];
115 | if (ok) {
116 | _hasSeenKeyDown = NO;
117 | _globalMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:(NSKeyDownMask | NSFlagsChangedMask) handler:^NSEvent*(NSEvent *event){
118 | [self processHotkeyEvent:event];
119 | return nil;
120 | }];
121 | }
122 | return ok;
123 | }
124 |
125 | - (BOOL)resignFirstResponder {
126 | BOOL ok = [super resignFirstResponder];
127 | if (ok) {
128 | self.hotKeyField = nil;
129 | if (_globalMonitor) {
130 | [NSEvent removeMonitor:_globalMonitor];
131 | _globalMonitor = nil;
132 | }
133 | }
134 |
135 | return ok;
136 | }
137 |
138 | @end
139 |
--------------------------------------------------------------------------------
/remember/vendor/DDHotKey/DDHotKeyUtilities.h:
--------------------------------------------------------------------------------
1 | /*
2 | DDHotKey -- DDHotKeyUtilities.h
3 |
4 | Copyright (c) Dave DeLong
5 |
6 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
7 |
8 | The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
9 | */
10 |
11 | #import
12 |
13 | extern NSString *DDStringFromKeyCode(unsigned short keyCode, NSUInteger modifiers);
14 | extern UInt32 DDCarbonModifierFlagsFromCocoaModifiers(NSUInteger flags);
15 |
--------------------------------------------------------------------------------
/remember/vendor/DDHotKey/DDHotKeyUtilities.m:
--------------------------------------------------------------------------------
1 | /*
2 | DDHotKey -- DDHotKeyUtilities.m
3 |
4 | Copyright (c) Dave DeLong
5 |
6 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
7 |
8 | The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
9 | */
10 |
11 | #import "DDHotKeyUtilities.h"
12 | #import
13 | #import
14 |
15 | static NSDictionary *_DDKeyCodeToCharacterMap(void);
16 | static NSDictionary *_DDKeyCodeToCharacterMap(void) {
17 | static NSDictionary *keyCodeMap = nil;
18 | static dispatch_once_t onceToken;
19 | dispatch_once(&onceToken, ^{
20 | keyCodeMap = @{
21 | @(kVK_Return) : @"↩",
22 | @(kVK_Tab) : @"⇥",
23 | @(kVK_Space) : @"⎵",
24 | @(kVK_Delete) : @"⌫",
25 | @(kVK_Escape) : @"⎋",
26 | @(kVK_Command) : @"⌘",
27 | @(kVK_Shift) : @"⇧",
28 | @(kVK_CapsLock) : @"⇪",
29 | @(kVK_Option) : @"⌥",
30 | @(kVK_Control) : @"⌃",
31 | @(kVK_RightShift) : @"⇧",
32 | @(kVK_RightOption) : @"⌥",
33 | @(kVK_RightControl) : @"⌃",
34 | @(kVK_VolumeUp) : @"🔊",
35 | @(kVK_VolumeDown) : @"🔈",
36 | @(kVK_Mute) : @"🔇",
37 | @(kVK_Function) : @"\u2318",
38 | @(kVK_F1) : @"F1",
39 | @(kVK_F2) : @"F2",
40 | @(kVK_F3) : @"F3",
41 | @(kVK_F4) : @"F4",
42 | @(kVK_F5) : @"F5",
43 | @(kVK_F6) : @"F6",
44 | @(kVK_F7) : @"F7",
45 | @(kVK_F8) : @"F8",
46 | @(kVK_F9) : @"F9",
47 | @(kVK_F10) : @"F10",
48 | @(kVK_F11) : @"F11",
49 | @(kVK_F12) : @"F12",
50 | @(kVK_F13) : @"F13",
51 | @(kVK_F14) : @"F14",
52 | @(kVK_F15) : @"F15",
53 | @(kVK_F16) : @"F16",
54 | @(kVK_F17) : @"F17",
55 | @(kVK_F18) : @"F18",
56 | @(kVK_F19) : @"F19",
57 | @(kVK_F20) : @"F20",
58 | // @(kVK_Help) : @"",
59 | @(kVK_ForwardDelete) : @"⌦",
60 | @(kVK_Home) : @"↖",
61 | @(kVK_End) : @"↘",
62 | @(kVK_PageUp) : @"⇞",
63 | @(kVK_PageDown) : @"⇟",
64 | @(kVK_LeftArrow) : @"←",
65 | @(kVK_RightArrow) : @"→",
66 | @(kVK_DownArrow) : @"↓",
67 | @(kVK_UpArrow) : @"↑",
68 | };
69 | });
70 | return keyCodeMap;
71 | }
72 |
73 | NSString *DDStringFromKeyCode(unsigned short keyCode, NSUInteger modifiers) {
74 | NSMutableString *final = [NSMutableString stringWithString:@""];
75 | NSDictionary *characterMap = _DDKeyCodeToCharacterMap();
76 |
77 | if (modifiers & NSControlKeyMask) {
78 | [final appendString:[characterMap objectForKey:@(kVK_Control)]];
79 | }
80 | if (modifiers & NSAlternateKeyMask) {
81 | [final appendString:[characterMap objectForKey:@(kVK_Option)]];
82 | }
83 | if (modifiers & NSShiftKeyMask) {
84 | [final appendString:[characterMap objectForKey:@(kVK_Shift)]];
85 | }
86 | if (modifiers & NSCommandKeyMask) {
87 | [final appendString:[characterMap objectForKey:@(kVK_Command)]];
88 | }
89 |
90 | if (keyCode == kVK_Control || keyCode == kVK_Option || keyCode == kVK_Shift || keyCode == kVK_Command) {
91 | return final;
92 | }
93 |
94 | NSString *mapped = [characterMap objectForKey:@(keyCode)];
95 | if (mapped != nil) {
96 | [final appendString:mapped];
97 | } else {
98 |
99 | TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
100 | CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
101 |
102 | // Fix crash using non-unicode layouts, such as Chinese or Japanese.
103 | if (!uchr) {
104 | CFRelease(currentKeyboard);
105 | currentKeyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
106 | uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
107 | }
108 |
109 | const UCKeyboardLayout *keyboardLayout = (const UCKeyboardLayout*)CFDataGetBytePtr(uchr);
110 |
111 | if (keyboardLayout) {
112 | UInt32 deadKeyState = 0;
113 | UniCharCount maxStringLength = 255;
114 | UniCharCount actualStringLength = 0;
115 | UniChar unicodeString[maxStringLength];
116 |
117 | UInt32 keyModifiers = DDCarbonModifierFlagsFromCocoaModifiers(modifiers);
118 |
119 | OSStatus status = UCKeyTranslate(keyboardLayout,
120 | keyCode, kUCKeyActionDown, keyModifiers,
121 | LMGetKbdType(), 0,
122 | &deadKeyState,
123 | maxStringLength,
124 | &actualStringLength, unicodeString);
125 |
126 | if (actualStringLength > 0 && status == noErr) {
127 | NSString *characterString = [NSString stringWithCharacters:unicodeString length:(NSUInteger)actualStringLength];
128 |
129 | [final appendString:characterString];
130 | }
131 | }
132 | }
133 |
134 | return final;
135 | }
136 |
137 | UInt32 DDCarbonModifierFlagsFromCocoaModifiers(NSUInteger flags) {
138 | UInt32 newFlags = 0;
139 | if ((flags & NSControlKeyMask) > 0) { newFlags |= controlKey; }
140 | if ((flags & NSCommandKeyMask) > 0) { newFlags |= cmdKey; }
141 | if ((flags & NSShiftKeyMask) > 0) { newFlags |= shiftKey; }
142 | if ((flags & NSAlternateKeyMask) > 0) { newFlags |= optionKey; }
143 | if ((flags & NSAlphaShiftKeyMask) > 0) { newFlags |= alphaLock; }
144 | return newFlags;
145 | }
146 |
--------------------------------------------------------------------------------
/remember/vendor/DDHotKey/README.markdown:
--------------------------------------------------------------------------------
1 | # DDHotKey
2 |
3 | Copyright © Dave DeLong
4 |
5 | ## About
6 |
7 | DDHotKey is an easy-to-use Cocoa class for registering an application to respond to system key events, or "hotkeys".
8 |
9 | A global hotkey is a key combination that always executes a specific action, regardless of which app is frontmost. For example, the Mac OS X default hotkey of "command-space" shows the Spotlight search bar, even if Finder is not the frontmost application.
10 |
11 | ## License
12 |
13 | The license for this framework is included in every source file, and is repoduced in its entirety here:
14 |
15 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
16 |
17 | ## Usage
18 |
19 | ### Including DDHotKey in your project
20 |
21 | You will need to copy these six files into your project:
22 |
23 | - DDHotKeyCenter.h
24 | - DDHotKeyCenter.m
25 | - DDHotKeyUtilities.h
26 | - DDHotKeyUtilities.m
27 | - DDHotKeyTextField.h
28 | - DDHotKeyTextField.m
29 |
30 | Your application will need to link against `Carbon.framework`, and you will need to compile your application with the Clang compiler. DDHotKey has been tested with Xcode 5 on OS X Mavericks. No attempt has been made to preserve backwards compatibility.
31 |
32 | ### Using DDHotKey in your code
33 |
34 | When you wish to create a hotkey, you'll need to do so via the `DDHotKeyCenter` singleton.
35 |
36 | You can register a hotkey in one of two ways: via a target/action mechanism, or with a block. The target/action mechanism can take a single extra "object" parameter, which it will pass into the action when the hotkey is fired. Only the `object` parameter is retained by the `DDHotKeyCenter`. In addition, an `NSEvent` object is passed, which contains information regarding the hotkey event (such as the location, the keyCode, the modifierFlags, etc).
37 |
38 | Hotkey actions must have one of two method signatures (the actual selector is irrelevant):
39 |
40 | //a method with a single NSEvent parameter
41 | - (void)hotkeyAction:(NSEvent*)hotKeyEvent;
42 |
43 | OR
44 |
45 | //a method with an NSEvent parameter and an object parameter
46 | - (void)hotkeyAction:(NSEvent*)hotKeyEvent withObject:(id)anObject;
47 |
48 | The other way to register a hotkey is with a block callback. The block must have the following signature:
49 |
50 | void (^)(NSEvent *);
51 |
52 | `DDHotKeyCenter.h` contains a typedef statement to typedef this signature as a `DDHotKeyTask`, for convenience.
53 |
54 | Any hotkey that you have registered via `DDHotKeyCenter` can be unregistered based on its target, its target and action, or its keycode and modifier flags.
55 |
56 | DDHotKey also includes a rudimentary `DDHotKeyTextField`, which is an `NSTextField` subclass that simplifies the process of creating a key combination. Simply drop an `NSTextField` into your xib and change its class to `DDHotKeyTextField`. Programmatically, you'll get an NSTextField into which you can type arbitrary key combinations. You access the resulting combination via the textfield's `hotKey` property.
57 |
--------------------------------------------------------------------------------
/tests/info.rkt:
--------------------------------------------------------------------------------
1 | #lang info
2 |
3 | (define collection "tests")
4 | (define deps
5 | '("base"
6 | "db-lib"
7 | "deta-lib"
8 | "gregor-lib"
9 | "rackunit-lib"
10 | "remember"
11 | "threading-lib"))
12 |
--------------------------------------------------------------------------------
/tests/remember/command.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require gregor
4 | racket/match
5 | rackunit
6 | remember/command)
7 |
8 | (check-equal?
9 | (parse-command "hello")
10 | (list
11 | (make-Token
12 | #:text "hello"
13 | #:span (Span
14 | (Location 1 0 0)
15 | (Location 1 5 5)))))
16 |
17 | (check-equal?
18 | (parse-command "hello +1d there")
19 | (list
20 | (make-Token
21 | #:text "hello "
22 | #:span (Span
23 | (Location 1 0 0)
24 | (Location 1 6 6)))
25 | (make-Token
26 | #:text "+1d"
27 | #:span (Span
28 | (Location 1 6 6)
29 | (Location 1 9 9))
30 | #:data (TokenData.relative-time 1 'd))
31 | (make-Token
32 | #:text " there"
33 | #:span (Span
34 | (Location 1 9 9)
35 | (Location 1 15 15)))))
36 |
37 | (check-equal?
38 | (parse-command "hello + there")
39 | (list
40 | (make-Token
41 | #:text "hello "
42 | #:span (Span
43 | (Location 1 0 0)
44 | (Location 1 6 6)))
45 | (make-Token
46 | #:text "+"
47 | #:span (Span
48 | (Location 1 6 6)
49 | (Location 1 7 7)))
50 | (make-Token
51 | #:text " there"
52 | #:span (Span
53 | (Location 1 7 7)
54 | (Location 1 13 13)))))
55 |
56 | (check-equal?
57 | (parse-command "buy milk +1d #groceries")
58 | (list
59 | (make-Token
60 | #:text "buy milk "
61 | #:span (Span
62 | (Location 1 0 0)
63 | (Location 1 9 9)))
64 | (make-Token
65 | #:text "+1d"
66 | #:span (Span
67 | (Location 1 9 9)
68 | (Location 1 12 12))
69 | #:data (TokenData.relative-time 1 'd))
70 | (make-Token
71 | #:text " "
72 | #:span (Span
73 | (Location 1 12 12)
74 | (Location 1 13 13)))
75 | (make-Token
76 | #:text "#groceries"
77 | #:span (Span
78 | (Location 1 13 13)
79 | (Location 1 23 23))
80 | #:data (TokenData.tag "groceries"))))
81 |
82 | (parameterize ([current-clock (lambda () 0)])
83 | (check-equal?
84 | (parse-command "buy milk @mon #groceries")
85 | (list
86 | (make-Token
87 | #:text "buy milk "
88 | #:span (Span
89 | (Location 1 0 0)
90 | (Location 1 9 9)))
91 | (make-Token
92 | #:text "@mon"
93 | #:span (Span
94 | (Location 1 9 9)
95 | (Location 1 13 13))
96 | #:data (TokenData.named-date
97 | (date 1970 1 5)))
98 | (make-Token
99 | #:text " "
100 | #:span (Span
101 | (Location 1 13 13)
102 | (Location 1 14 14)))
103 | (make-Token
104 | #:text "#groceries"
105 | #:span (Span
106 | (Location 1 14 14)
107 | (Location 1 24 24))
108 | #:data (TokenData.tag "groceries"))))
109 |
110 | (check-equal?
111 | (parse-command "buy milk @thu #groceries")
112 | (list
113 | (make-Token
114 | #:text "buy milk "
115 | #:span (Span
116 | (Location 1 0 0)
117 | (Location 1 9 9)))
118 | (make-Token
119 | #:text "@thu"
120 | #:span (Span
121 | (Location 1 9 9)
122 | (Location 1 13 13))
123 | #:data (TokenData.named-date
124 | (date 1970 1 8)))
125 | (make-Token
126 | #:text " "
127 | #:span (Span
128 | (Location 1 13 13)
129 | (Location 1 14 14)))
130 | (make-Token
131 | #:text "#groceries"
132 | #:span (Span
133 | (Location 1 14 14)
134 | (Location 1 24 24))
135 | #:data (TokenData.tag "groceries"))))
136 |
137 | (check-equal?
138 | (parse-command "buy milk @3pm #groceries")
139 | (list
140 | (make-Token
141 | #:text "buy milk "
142 | #:span (Span
143 | (Location 1 0 0)
144 | (Location 1 9 9)))
145 | (make-Token
146 | #:text "@3pm "
147 | #:span (Span
148 | (Location 1 9 9)
149 | (Location 1 14 14))
150 | #:data (TokenData.named-datetime
151 | (datetime 1970 1 1 15)))
152 | (make-Token
153 | #:text "#groceries"
154 | #:span (Span
155 | (Location 1 14 14)
156 | (Location 1 24 24))
157 | #:data (TokenData.tag "groceries"))))
158 |
159 | (check-equal?
160 | (parse-command "buy milk @3:15pm tmw #groceries")
161 | (list
162 | (make-Token
163 | #:text "buy milk "
164 | #:span (Span
165 | (Location 1 0 0)
166 | (Location 1 9 9)))
167 | (make-Token
168 | #:text "@3:15pm tmw"
169 | #:span (Span
170 | (Location 1 9 9)
171 | (Location 1 20 20))
172 | #:data (TokenData.named-datetime
173 | (datetime 1970 1 2 15 15)))
174 | (make-Token
175 | #:text " "
176 | #:span (Span
177 | (Location 1 20 20)
178 | (Location 1 21 21)))
179 | (make-Token
180 | #:text "#groceries"
181 | #:span (Span
182 | (Location 1 21 21)
183 | (Location 1 31 31))
184 | #:data (TokenData.tag "groceries"))))
185 |
186 | (check-equal?
187 | (parse-command "invoice Jim @10am mon *weekly*")
188 | (list
189 | (make-Token
190 | #:text "invoice Jim "
191 | #:span (Span
192 | (Location 1 0 0)
193 | (Location 1 12 12)))
194 | (make-Token
195 | #:text "@10am mon"
196 | #:span (Span
197 | (Location 1 12 12)
198 | (Location 1 21 21))
199 | #:data (TokenData.named-datetime
200 | (datetime 1970 1 5 10)))
201 | (make-Token
202 | #:text " "
203 | #:span (Span
204 | (Location 1 21 21)
205 | (Location 1 22 22)))
206 | (make-Token
207 | #:text "*weekly*"
208 | #:span (Span
209 | (Location 1 22 22)
210 | (Location 1 30 30))
211 | #:data (TokenData.recurrence 1 'week))))
212 |
213 | (check-equal?
214 | (parse-command "invoice Jim @10am mon *every 2 weeks*")
215 | (list
216 | (make-Token
217 | #:text "invoice Jim "
218 | #:span (Span
219 | (Location 1 0 0)
220 | (Location 1 12 12)))
221 | (make-Token
222 | #:text "@10am mon"
223 | #:span (Span
224 | (Location 1 12 12)
225 | (Location 1 21 21))
226 | #:data (TokenData.named-datetime
227 | (datetime 1970 1 5 10)))
228 | (make-Token
229 | #:text " "
230 | #:span (Span
231 | (Location 1 21 21)
232 | (Location 1 22 22)))
233 | (make-Token
234 | #:text "*every 2 weeks*"
235 | #:span (Span
236 | (Location 1 22 22)
237 | (Location 1 37 37))
238 | #:data (TokenData.recurrence 2 'week))))
239 |
240 | (define-check (check-named-datetime command expected)
241 | (match-define (TokenData.named-datetime datetime)
242 | (Token-data (car (parse-command command))))
243 | (check-equal? datetime expected))
244 |
245 | (check-named-datetime "@9am" (datetime 1970 1 1 9 0 0 0))
246 | (check-named-datetime "@09am" (datetime 1970 1 1 9 0 0 0))
247 | (check-named-datetime "@10pm" (datetime 1970 1 1 22 0 0 0))
248 | (check-named-datetime "@10:35pm" (datetime 1970 1 1 22 35 0 0))
249 | (check-named-datetime "@10:35pm tmw" (datetime 1970 1 2 22 35 0 0))
250 | (check-named-datetime "@09" (datetime 1970 1 1 9 0 0 0))
251 | (check-named-datetime "@09:59 mon" (datetime 1970 1 5 9 59 0 0))
252 | (check-named-datetime "@22" (datetime 1970 1 1 22 0 0 0))
253 | (check-named-datetime "@22:35" (datetime 1970 1 1 22 35 0 0))
254 | (check-named-datetime "@22:35 tmw" (datetime 1970 1 2 22 35 0 0))
255 | (check-named-datetime "@25:59 mon" (datetime 1970 1 1 2 0 0 0)))
256 |
--------------------------------------------------------------------------------
/tests/remember/ring.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require rackunit
4 | remember/ring)
5 |
6 | (define r (make-ring 3))
7 | (ring-push! r 1)
8 | (ring-push! r 2)
9 | (check-equal? (ring-size r) 2)
10 | (check-eqv? (ring-pop! r) 2)
11 |
12 | (ring-push! r 2)
13 | (ring-push! r 3)
14 | (ring-push! r 4)
15 | (check-equal? (ring-size r) 3)
16 | (check-eqv? (ring-pop! r) 4)
17 | (check-eqv? (ring-pop! r) 3)
18 | (check-eqv? (ring-pop! r) 2)
19 | (check-false (ring-pop! r))
20 | (check-false (ring-pop! r))
21 |
22 | (ring-push! r 1)
23 | (check-eqv? (ring-pop! r) 1)
24 | (check-false (ring-pop! r))
25 |
--------------------------------------------------------------------------------
/tests/remember/undo.rkt:
--------------------------------------------------------------------------------
1 | #lang racket/base
2 |
3 | (require rackunit
4 | remember/ring
5 | remember/undo)
6 |
7 | (parameterize ([current-undo-ring (make-ring 128)])
8 | (define x #f)
9 |
10 | (push-undo! (lambda () (set! x 1)))
11 | (push-undo! (lambda () (set! x 2)))
12 | (push-undo! (lambda () (set! x 3)))
13 |
14 | (undo!)
15 | (check-eqv? x 3)
16 |
17 | (undo!)
18 | (check-eqv? x 2)
19 |
20 | (undo!)
21 | (check-eqv? x 1)
22 |
23 | (undo!)
24 | (check-eqv? x 1))
25 |
--------------------------------------------------------------------------------
/website/.gitattributes:
--------------------------------------------------------------------------------
1 | assets/demo-adding.mp4 filter=lfs diff=lfs merge=lfs -text
2 | assets/demo-listing.mp4 filter=lfs diff=lfs merge=lfs -text
3 | assets/demo-notify.mp4 filter=lfs diff=lfs merge=lfs -text
4 | assets/demo.mp4 filter=lfs diff=lfs merge=lfs -text
5 |
--------------------------------------------------------------------------------
/website/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: deploy
2 | deploy:
3 | rsync --exclude Makefile -avh . remember:~/www/
4 |
--------------------------------------------------------------------------------
/website/assets/.gitattributes:
--------------------------------------------------------------------------------
1 | demo-adding.gif filter=lfs diff=lfs merge=lfs -text
2 | demo-listing.gif filter=lfs diff=lfs merge=lfs -text
3 | demo-notify.gif filter=lfs diff=lfs merge=lfs -text
4 | demo.gif filter=lfs diff=lfs merge=lfs -text
5 |
--------------------------------------------------------------------------------
/website/assets/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all
2 | all: demo.gif demo-adding.gif demo-listing.gif demo-notify.gif
3 |
4 | %.gif: %.mp4
5 | ffmpeg -i $< -vf "fps=15,scale=520:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 $@
6 |
--------------------------------------------------------------------------------
/website/assets/demo-adding.gif:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:66043025d951e8a8ec8797815839446301a4f1ff3a37bfc4e45b937676f04cb3
3 | size 386341
4 |
--------------------------------------------------------------------------------
/website/assets/demo-adding.mp4:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:19f33ccbb8a7512eb99143fdd7a63c9bef463edf4b6204c6d52bc3072bd5d549
3 | size 922066
4 |
--------------------------------------------------------------------------------
/website/assets/demo-listing.gif:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:ed073c6b44639e5fd801b27ec7a9d3efe49186b85e1ab77854c1a1c6dbfb8d8c
3 | size 446319
4 |
--------------------------------------------------------------------------------
/website/assets/demo-listing.mp4:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:4e3202f1a4ea2189366f2d299ebc0af727cf3254fdd6524239d0f02596da96a8
3 | size 789479
4 |
--------------------------------------------------------------------------------
/website/assets/demo-notify.gif:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:4ffbc37cbb896693cc23f20565d623dec0373e28833a12f304fdc612087ba9b8
3 | size 880848
4 |
--------------------------------------------------------------------------------
/website/assets/demo-notify.mp4:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:af9cfe7b0f6f669bb579ab44bc1a227b61542500ca7b62154460778ac07a056e
3 | size 1782308
4 |
--------------------------------------------------------------------------------
/website/assets/demo.gif:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:04a7c42da18cd4948a887850b2fc39921ed689f3434bb8d37becd4a105d2087b
3 | size 2454397
4 |
--------------------------------------------------------------------------------
/website/assets/demo.mp4:
--------------------------------------------------------------------------------
1 | version https://git-lfs.github.com/spec/v1
2 | oid sha256:3ac50348c0dc439096d73be87acb2b945f02c553ea550ef6b0c9e17f729a8cfb
3 | size 3985006
4 |
--------------------------------------------------------------------------------
/website/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bogdanp/remember/e5749f58776ad7f00a0ecfe5f4db8a5c6d9b5ffd/website/assets/logo.png
--------------------------------------------------------------------------------
/website/manual/manual-racket.css:
--------------------------------------------------------------------------------
1 | /* See the beginning of "manual.css". */
2 |
3 | /* Monospace: */
4 |
5 | .RktIn, .RktRdr, .RktPn, .RktMeta,
6 | .RktMod, .RktKw, .RktVar, .RktSym,
7 | .RktRes, .RktOut, .RktCmt, .RktVal,
8 | .RktBlk, .RktErr {
9 | font-family: 'Fira-Mono', monospace;
10 | white-space: inherit;
11 | font-size: 1rem;
12 | line-height: 1.5;
13 |
14 | }
15 |
16 | /* this selctor grabs the first linked Racket symbol
17 | in a definition box (i.e., the symbol being defined) */
18 | a.RktValDef, a.RktStxDef, a.RktSymDef,
19 | span.RktValDef, span.RktStxDef, span.RktSymDef
20 | {
21 | font-size: 1.1rem;
22 | color: black;
23 | font-weight: 500;
24 | }
25 |
26 |
27 | .inheritedlbl {
28 | font-family: 'Fira', sans-serif;
29 | }
30 |
31 | .RBackgroundLabelInner {
32 | font-family: inherit;
33 | }
34 |
35 | /* ---------------------------------------- */
36 | /* Inherited methods, left margin */
37 |
38 | .inherited {
39 | width: 95%;
40 | margin-top: 0.5em;
41 | text-align: left;
42 | background-color: inherit;
43 | }
44 |
45 | .inherited td {
46 | font-size: 82%;
47 | padding-left: 0.5rem;
48 | line-height: 1.3;
49 | text-indent: 0;
50 | padding-right: 0;
51 | }
52 |
53 | .inheritedlbl {
54 | font-style: normal;
55 | }
56 |
57 | /* ---------------------------------------- */
58 | /* Racket text styles */
59 |
60 | .RktIn {
61 | color: #cc6633;
62 | background-color: #eee;
63 | }
64 |
65 | .RktInBG {
66 | background-color: #eee;
67 | }
68 |
69 |
70 | .refcolumn .RktInBG {
71 | background-color: white;
72 | }
73 |
74 | .RktRdr {
75 | }
76 |
77 | .RktPn {
78 | color: #843c24;
79 | }
80 |
81 | .RktMeta {
82 | color: black;
83 | }
84 |
85 | .RktMod {
86 | color: inherit;
87 | }
88 |
89 | .RktOpt {
90 | color: black;
91 | font-style: italic;
92 | }
93 |
94 | .RktKw {
95 | color: black;
96 | }
97 |
98 | .RktErr {
99 | color: red;
100 | font-style: italic;
101 | font-weight: 400;
102 | }
103 |
104 | .RktVar {
105 | position: relative;
106 | left: -1px; font-style: italic;
107 | color: #444;
108 | }
109 |
110 | .SVInsetFlow .RktVar {
111 | font-weight: 400;
112 | color: #444;
113 | }
114 |
115 |
116 | .RktSym {
117 | color: inherit;
118 | }
119 |
120 |
121 |
122 | .RktValLink, .RktStxLink, .RktModLink {
123 | text-decoration: none;
124 | color: #07A;
125 | font-size: 1rem;
126 | }
127 |
128 | /* for syntax links within headings */
129 | h2 a.RktStxLink, h3 a.RktStxLink, h4 a.RktStxLink, h5 a.RktStxLink,
130 | h2 a.RktValLink, h3 a.RktValLink, h4 a.RktValLink, h5 a.RktValLink,
131 | h2 .RktSym, h3 .RktSym, h4 .RktSym, h5 .RktSym,
132 | h2 .RktMod, h3 .RktMod, h4 .RktMod, h5 .RktMod,
133 | h2 .RktVal, h3 .RktVal, h4 .RktVal, h5 .RktVal,
134 | h2 .RktPn, h3 .RktPn, h4 .RktPn, h5 .RktPn {
135 | color: #333;
136 | font-size: 1.50rem;
137 | font-weight: 400;
138 | }
139 |
140 | .toptoclink .RktStxLink, .toclink .RktStxLink,
141 | .toptoclink .RktValLink, .toclink .RktValLink,
142 | .toptoclink .RktModLink, .toclink .RktModLink {
143 | color: inherit;
144 | }
145 |
146 | .tocset .RktValLink, .tocset .RktStxLink, .tocset .RktModLink, .tocset .RktSym {
147 | color: black;
148 | font-weight: 400;
149 | font-size: 0.9rem;
150 | }
151 |
152 | .tocset td a.tocviewselflink .RktValLink,
153 | .tocset td a.tocviewselflink .RktStxLink,
154 | .tocset td a.tocviewselflink .RktMod,
155 | .tocset td a.tocviewselflink .RktSym {
156 | font-weight: lighter;
157 | color: white;
158 | }
159 |
160 |
161 | .RktRes {
162 | color: #0000af;
163 | }
164 |
165 | .RktOut {
166 | color: #960096;
167 | }
168 |
169 | .RktCmt {
170 | color: #c2741f;
171 | }
172 |
173 | .RktVal {
174 | color: #228b22;
175 | }
176 |
177 | /* ---------------------------------------- */
178 | /* Some inline styles */
179 |
180 | .together { /* for definitions grouped together in one box */
181 | width: 100%;
182 | border-top: 2px solid white;
183 | }
184 |
185 | tbody > tr:first-child > td > .together {
186 | border-top: 0px; /* erase border on first instance of together */
187 | }
188 |
189 | .RktBlk {
190 | white-space: pre;
191 | text-align: left;
192 | }
193 |
194 | .highlighted {
195 | font-size: 1rem;
196 | background-color: #fee;
197 | }
198 |
199 | .defmodule {
200 | font-family: 'Fira-Mono', monospace;
201 | padding: 0.25rem 0.75rem 0.25rem 0.5rem;
202 | margin-bottom: 1rem;
203 | width: 100%;
204 | background-color: #ebf0f4;
205 | }
206 |
207 | .defmodule a {
208 | color: #444;
209 | }
210 |
211 |
212 | .defmodule td span.hspace:first-child {
213 | position: absolute;
214 | width: 0;
215 | display: inline-block;
216 | }
217 |
218 | .defmodule .RpackageSpec .Smaller,
219 | .defmodule .RpackageSpec .stt {
220 | font-size: 1rem;
221 | }
222 |
223 | /* make parens ordinary color in defmodule */
224 | .defmodule .RktPn {
225 | color: inherit;
226 | }
227 |
228 | .specgrammar {
229 | float: none;
230 | padding-left: 1em;
231 | }
232 |
233 |
234 | .RBibliography td {
235 | vertical-align: text-top;
236 | padding-top: 1em;
237 | }
238 |
239 | .leftindent {
240 | margin-left: 2rem;
241 | margin-right: 0em;
242 | }
243 |
244 | .insetpara {
245 | margin-left: 1em;
246 | margin-right: 1em;
247 | }
248 |
249 | .SCodeFlow .Rfilebox {
250 | margin-left: -1em; /* see 17.2 of guide, module languages */
251 | }
252 |
253 | .Rfiletitle {
254 | text-align: right;
255 | background-color: #eee;
256 | }
257 |
258 | .SCodeFlow .Rfiletitle {
259 | border-top: 1px dotted gray;
260 | border-right: 1px dotted gray;
261 | }
262 |
263 |
264 | .Rfilename {
265 | border-top: 0;
266 | border-right: 0;
267 | padding-left: 0.5em;
268 | padding-right: 0.5em;
269 | background-color: inherit;
270 | }
271 |
272 | .Rfilecontent {
273 | margin: 0.5em;
274 | }
275 |
276 | .RpackageSpec {
277 | padding-right: 0;
278 | }
279 |
280 | /* ---------------------------------------- */
281 | /* For background labels */
282 |
283 | .RBackgroundLabel {
284 | float: right;
285 | width: 0px;
286 | height: 0px;
287 | }
288 |
289 | .RBackgroundLabelInner {
290 | position: relative;
291 | width: 25em;
292 | left: -25.5em;
293 | top: 0.20rem; /* sensitive to monospaced font choice */
294 | text-align: right;
295 | z-index: 0;
296 | font-weight: 300;
297 | font-family: 'Fira-Mono', monospace;
298 | font-size: 0.9rem;
299 | color: gray;
300 | }
301 |
302 |
303 | .RpackageSpec .Smaller {
304 | font-weight: 300;
305 | font-family: 'Fira-Mono', monospace;
306 | font-size: 0.9rem;
307 | }
308 |
309 | .RForeground {
310 | position: relative;
311 | left: 0px;
312 | top: 0px;
313 | z-index: 1;
314 | }
315 |
316 | /* ---------------------------------------- */
317 | /* For section source modules & tags */
318 |
319 | .RPartExplain {
320 | background: #eee;
321 | font-size: 0.9rem;
322 | margin-top: 0.2rem;
323 | padding: 0.2rem;
324 | text-align: left;
325 | }
326 |
--------------------------------------------------------------------------------
/website/manual/racket.css:
--------------------------------------------------------------------------------
1 |
2 | /* See the beginning of "scribble.css". */
3 |
4 | /* Monospace: */
5 | .RktIn, .RktRdr, .RktPn, .RktMeta,
6 | .RktMod, .RktKw, .RktVar, .RktSym,
7 | .RktRes, .RktOut, .RktCmt, .RktVal,
8 | .RktBlk {
9 | font-family: monospace;
10 | white-space: inherit;
11 | }
12 |
13 | /* Serif: */
14 | .inheritedlbl {
15 | font-family: serif;
16 | }
17 |
18 | /* Sans-serif: */
19 | .RBackgroundLabelInner {
20 | font-family: sans-serif;
21 | }
22 |
23 | /* ---------------------------------------- */
24 | /* Inherited methods, left margin */
25 |
26 | .inherited {
27 | width: 100%;
28 | margin-top: 0.5em;
29 | text-align: left;
30 | background-color: #ECF5F5;
31 | }
32 |
33 | .inherited td {
34 | font-size: 82%;
35 | padding-left: 1em;
36 | text-indent: -0.8em;
37 | padding-right: 0.2em;
38 | }
39 |
40 | .inheritedlbl {
41 | font-style: italic;
42 | }
43 |
44 | /* ---------------------------------------- */
45 | /* Racket text styles */
46 |
47 | .RktIn {
48 | color: #cc6633;
49 | background-color: #eeeeee;
50 | }
51 |
52 | .RktInBG {
53 | background-color: #eeeeee;
54 | }
55 |
56 | .RktRdr {
57 | }
58 |
59 | .RktPn {
60 | color: #843c24;
61 | }
62 |
63 | .RktMeta {
64 | color: black;
65 | }
66 |
67 | .RktMod {
68 | color: black;
69 | }
70 |
71 | .RktOpt {
72 | color: black;
73 | font-style: italic;
74 | }
75 |
76 | .RktKw {
77 | color: black;
78 | }
79 |
80 | .RktErr {
81 | color: red;
82 | font-style: italic;
83 | }
84 |
85 | .RktVar {
86 | color: #262680;
87 | font-style: italic;
88 | }
89 |
90 | .RktSym {
91 | color: #262680;
92 | }
93 |
94 | .RktSymDef { /* used with RktSym at def site */
95 | }
96 |
97 | .RktValLink {
98 | text-decoration: none;
99 | color: blue;
100 | }
101 |
102 | .RktValDef { /* used with RktValLink at def site */
103 | }
104 |
105 | .RktModLink {
106 | text-decoration: none;
107 | color: blue;
108 | }
109 |
110 | .RktStxLink {
111 | text-decoration: none;
112 | color: black;
113 | }
114 |
115 | .RktStxDef { /* used with RktStxLink at def site */
116 | }
117 |
118 | .RktRes {
119 | color: #0000af;
120 | }
121 |
122 | .RktOut {
123 | color: #960096;
124 | }
125 |
126 | .RktCmt {
127 | color: #c2741f;
128 | }
129 |
130 | .RktVal {
131 | color: #228b22;
132 | }
133 |
134 | /* ---------------------------------------- */
135 | /* Some inline styles */
136 |
137 | .together {
138 | width: 100%;
139 | }
140 |
141 | .prototype, .argcontract, .RBoxed {
142 | white-space: nowrap;
143 | }
144 |
145 | .prototype td {
146 | vertical-align: text-top;
147 | }
148 |
149 | .RktBlk {
150 | white-space: inherit;
151 | text-align: left;
152 | }
153 |
154 | .RktBlk tr {
155 | white-space: inherit;
156 | }
157 |
158 | .RktBlk td {
159 | vertical-align: baseline;
160 | white-space: inherit;
161 | }
162 |
163 | .argcontract td {
164 | vertical-align: text-top;
165 | }
166 |
167 | .highlighted {
168 | background-color: #ddddff;
169 | }
170 |
171 | .defmodule {
172 | width: 100%;
173 | background-color: #F5F5DC;
174 | }
175 |
176 | .specgrammar {
177 | float: right;
178 | }
179 |
180 | .RBibliography td {
181 | vertical-align: text-top;
182 | }
183 |
184 | .leftindent {
185 | margin-left: 1em;
186 | margin-right: 0em;
187 | }
188 |
189 | .insetpara {
190 | margin-left: 1em;
191 | margin-right: 1em;
192 | }
193 |
194 | .Rfilebox {
195 | }
196 |
197 | .Rfiletitle {
198 | text-align: right;
199 | margin: 0em 0em 0em 0em;
200 | }
201 |
202 | .Rfilename {
203 | border-top: 1px solid #6C8585;
204 | border-right: 1px solid #6C8585;
205 | padding-left: 0.5em;
206 | padding-right: 0.5em;
207 | background-color: #ECF5F5;
208 | }
209 |
210 | .Rfilecontent {
211 | margin: 0em 0em 0em 0em;
212 | }
213 |
214 | .RpackageSpec {
215 | padding-right: 0.5em;
216 | }
217 |
218 | /* ---------------------------------------- */
219 | /* For background labels */
220 |
221 | .RBackgroundLabel {
222 | float: right;
223 | width: 0px;
224 | height: 0px;
225 | }
226 |
227 | .RBackgroundLabelInner {
228 | position: relative;
229 | width: 25em;
230 | left: -25.5em;
231 | top: 0px;
232 | text-align: right;
233 | color: white;
234 | z-index: 0;
235 | font-weight: bold;
236 | }
237 |
238 | .RForeground {
239 | position: relative;
240 | left: 0px;
241 | top: 0px;
242 | z-index: 1;
243 | }
244 |
245 | /* ---------------------------------------- */
246 | /* History */
247 |
248 | .SHistory {
249 | font-size: 82%;
250 | }
251 |
--------------------------------------------------------------------------------
/website/manual/scribble-common.js:
--------------------------------------------------------------------------------
1 | // Common functionality for PLT documentation pages
2 |
3 | // Page Parameters ------------------------------------------------------------
4 |
5 | var page_query_string = location.search.substring(1);
6 |
7 | var page_args =
8 | ((function(){
9 | if (!page_query_string) return [];
10 | var args = page_query_string.split(/[&;]/);
11 | for (var i=0; i= 0) args[i] = [a.substring(0,p), a.substring(p+1)];
15 | else args[i] = [a, false];
16 | }
17 | return args;
18 | })());
19 |
20 | function GetPageArg(key, def) {
21 | for (var i=0; i= 0 && cur.substring(0,eql) == key)
78 | return unescape(cur.substring(eql+1));
79 | }
80 | return def;
81 | }
82 | }
83 |
84 | function SetCookie(key, val) {
85 | try {
86 | localStorage[key] = val;
87 | } catch(e) {
88 | var d = new Date();
89 | d.setTime(d.getTime()+(365*24*60*60*1000));
90 | try {
91 | document.cookie =
92 | key + "=" + escape(val) + "; expires="+ d.toGMTString() + "; path=/";
93 | } catch (e) {}
94 | }
95 | }
96 |
97 | // note that this always stores a directory name, ending with a "/"
98 | function SetPLTRoot(ver, relative) {
99 | var root = location.protocol + "//" + location.host
100 | + NormalizePath(location.pathname.replace(/[^\/]*$/, relative));
101 | SetCookie("PLT_Root."+ver, root);
102 | }
103 |
104 | // adding index.html works because of the above
105 | function GotoPLTRoot(ver, relative) {
106 | var u = GetCookie("PLT_Root."+ver, null);
107 | if (u == null) return true; // no cookie: use plain up link
108 | // the relative path is optional, default goes to the toplevel start page
109 | if (!relative) relative = "index.html";
110 | location = u + relative;
111 | return false;
112 | }
113 |
114 | // Utilities ------------------------------------------------------------------
115 |
116 | var normalize_rxs = [/\/\/+/g, /\/\.(\/|$)/, /\/[^\/]*\/\.\.(\/|$)/];
117 | function NormalizePath(path) {
118 | var tmp, i;
119 | for (i = 0; i < normalize_rxs.length; i++)
120 | while ((tmp = path.replace(normalize_rxs[i], "/")) != path) path = tmp;
121 | return path;
122 | }
123 |
124 | // `noscript' is problematic in some browsers (always renders as a
125 | // block), use this hack instead (does not always work!)
126 | // document.write("");
127 |
128 | // Interactions ---------------------------------------------------------------
129 |
130 | function DoSearchKey(event, field, ver, top_path) {
131 | var val = field.value;
132 | if (event && event.key === 'Enter') {
133 | var u = GetCookie("PLT_Root."+ver, null);
134 | if (u == null) u = top_path; // default: go to the top path
135 | u += "search/index.html?q=" + encodeURIComponent(val);
136 | u = MergePageArgsIntoUrl(u);
137 | location = u;
138 | return false;
139 | }
140 | return true;
141 | }
142 |
143 | function TocviewToggle(glyph, id) {
144 | var s = document.getElementById(id).style;
145 | var expand = s.display == "none";
146 | s.display = expand ? "block" : "none";
147 | glyph.innerHTML = expand ? "▼" : "►";
148 | }
149 |
150 | function TocsetToggle() {
151 | document.body.classList.toggle("tocsetoverlay");
152 | }
153 |
154 | // Page Init ------------------------------------------------------------------
155 |
156 | // Note: could make a function that inspects and uses window.onload to chain to
157 | // a previous one, but this file needs to be required first anyway, since it
158 | // contains utilities for all other files.
159 | var on_load_funcs = [];
160 | function AddOnLoad(fun) { on_load_funcs.push(fun); }
161 | window.onload = function() {
162 | for (var i=0; i