diff --git a/.gitmodules b/.gitmodules index 139a394a72e..83aedc535f3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -600,3 +600,7 @@ path = _build/cl-json url = https://github.com/sharplispers/cl-json shallow = true +[submodule "_build/cl-sqlite"] + path = _build/cl-sqlite + url = https://github.com/TeMPOraL/cl-sqlite + shallow = true diff --git a/_build/cl-sqlite b/_build/cl-sqlite new file mode 160000 index 00000000000..be2fcc193f9 --- /dev/null +++ b/_build/cl-sqlite @@ -0,0 +1 @@ +Subproject commit be2fcc193f98e3d5bdc85958a806d612cc48740c diff --git a/build-scripts/nyxt.scm b/build-scripts/nyxt.scm index d841d0d9b9c..57ca4714c29 100644 --- a/build-scripts/nyxt.scm +++ b/build-scripts/nyxt.scm @@ -184,6 +184,7 @@ cl-str cl-slime-swank cl-slynk + cl-sqlite cl-tld cl-trivia cl-trivial-clipboard diff --git a/documents/SOURCES.org b/documents/SOURCES.org index fdae8457f4a..c2740cf6489 100644 --- a/documents/SOURCES.org +++ b/documents/SOURCES.org @@ -27,6 +27,7 @@ Experimental support. - cl-ppcre-unicode - cl-prevalence - cl-qrencode +- cl-sqlite - cl-tld - closer-mop - cluffer diff --git a/nyxt.asd b/nyxt.asd index 6b616a7e7e2..8d5d708e260 100644 --- a/nyxt.asd +++ b/nyxt.asd @@ -111,6 +111,7 @@ The renderer is configured from NYXT_RENDERER or `*nyxt-renderer*'.")) clss spinneret slynk + sqlite swank trivia trivial-clipboard @@ -240,6 +241,7 @@ The renderer is configured from NYXT_RENDERER or `*nyxt-renderer*'.")) (:file "emacs") (:file "expedition") (:file "force-https") + (:file "history-migration") (:file "macro-edit") (:file "no-image") (:file "no-procrastinate" :depends-on ("blocker")) diff --git a/source/changelog.lisp b/source/changelog.lisp index b20c10b983c..6b527dda8d0 100644 --- a/source/changelog.lisp +++ b/source/changelog.lisp @@ -809,6 +809,12 @@ buffers.") (:li "Fix styling of progress bar.") (:li "Fix styling of prompt buffer's input area.")))) +(define-version "3.X.Y" + (:ul + (:li "Add commands for importing history from other browsers. +Currently, the supported browsers are Firefox, Google Chrome, +Chromium, Brave and Vivaldi."))) + (define-version "4-pre-release-1" (:li "When on pre-release, push " (:code "X-pre-release") " feature in addition to " (:code "X-pre-release-N") "one.")) diff --git a/source/mode/history-migration.lisp b/source/mode/history-migration.lisp new file mode 100644 index 00000000000..5267367d41f --- /dev/null +++ b/source/mode/history-migration.lisp @@ -0,0 +1,115 @@ +;;;; SPDX-FileCopyrightText: Atlas Engineer LLC +;;;; SPDX-License-Identifier: BSD-3-Clause + +(nyxt:define-package :nyxt/mode/history-migration + (:documentation "Package for `history-migration-mode', mode to import history from other +browsers.")) +(in-package :nyxt/mode/history-migration) + +(define-mode history-migration-mode () + "Mode for importing history from other browsers." + ((visible-in-status-p nil) + (rememberable-p nil)) + (:toggler-command-p nil)) + +(define-class external-browser-history-source (prompter:source) + ((prompter:name "History files") + (browser-lambda :accessor browser-lambda :initarg :browser-lambda) + (prompter:constructor + (lambda (source) + (echo "Searching for history file. This may take some time.") + (or (let ((results '())) + (uiop:collect-sub*directories + (user-homedir-pathname) + (constantly t) + (lambda (subdir) + (equal (iolib/os:file-kind subdir) :directory)) + (lambda (subdir) + (let ((browser-history-file (funcall* (browser-lambda source) subdir))) + (when browser-history-file + (push browser-history-file results))))) + results) + (echo-warning "No history files found.")))))) + +(defmacro define-history-import-command (name docstring &key sql-query file-path) + "Shorthand to define a global command for importing history from a browser. +Make sure that the sql-query is a SELECT statement that selects: + - url + - title + - last-access (unix time in seconds) + - visits +Or the equivalent columns for the browser in question." + `(define-command-global ,name () + ,docstring + (let ((db-path ,file-path)) + (files:with-file-content (history (history-file (current-buffer)) + :default (nyxt::make-history-tree)) + (handler-bind ((sqlite:sqlite-error + (lambda (_) + (declare (ignore _)) + (echo-warning "Please close the browser you wish to import history from before running this command.") + (invoke-restart 'abort)))) + (sqlite:with-open-database (db db-path) + (echo "Importing history from ~a." db-path) + (loop for (url title last-access visits) in (sqlite:execute-to-list db ,sql-query) + do (unless (url-empty-p url) + (htree:add-entry history + (make-instance 'history-entry + :url url + :title title + :implicit-visits visits) + (local-time:unix-to-timestamp last-access)))) + (echo "History import finished."))))))) + +(define-history-import-command import-history-from-firefox + "Import history from Mozilla Firefox." + :sql-query "SELECT url, title, last_visit_date/1000000, visit_count FROM moz_places WHERE last_visit_date not null" + :file-path (prompt1 :prompt "Choose Mozilla Firefox places.sqlite file" + :sources (make-instance 'external-browser-history-source + :browser-lambda (lambda (subdir) + (when (uiop:file-exists-p (uiop:merge-pathnames* "places.sqlite" subdir)) + (uiop:merge-pathnames* "places.sqlite" subdir)))))) + +(define-history-import-command import-history-from-google-chrome + "Import history from Google Chrome." + :sql-query "SELECT url, title, last_visit_time/1000000-11644473600, visit_count FROM urls" + :file-path (prompt1 :prompt "Choose Google Chrome History file" + :sources (make-instance 'external-browser-history-source + :browser-lambda (lambda (subdir) + (when (and (string= "google-chrome" + (nfiles:basename (nfiles:parent subdir))) + (uiop:file-exists-p (uiop:merge-pathnames* "History" subdir))) + (uiop:merge-pathnames* "History" subdir)))))) + +(define-history-import-command import-history-from-chromium + "Import history from Chromium." + :sql-query "SELECT url, title, last_visit_time/1000000-11644473600, visit_count FROM urls" + :file-path (prompt1 :prompt "Choose Chromium History file" + :sources (make-instance 'external-browser-history-source + :browser-lambda (lambda (subdir) + (when (and (string= "chromium" + (nfiles:basename (nfiles:parent subdir))) + (uiop:file-exists-p (uiop:merge-pathnames* "History" subdir))) + (uiop:merge-pathnames* "History" subdir)))))) + +(define-history-import-command import-history-from-brave + "Import history from Brave." + :sql-query "SELECT url, title, last_visit_time/1000000-11644473600, visit_count FROM urls" + :file-path (prompt1 :prompt "Choose Brave History file" + :sources (make-instance 'external-browser-history-source + :browser-lambda (lambda (subdir) + (when (and (string= "Brave-Browser" + (nfiles:basename (nfiles:parent subdir))) + (uiop:file-exists-p (uiop:merge-pathnames* "History" subdir))) + (uiop:merge-pathnames* "History" subdir)))))) + +(define-history-import-command import-history-from-vivaldi + "Import history from Vivaldi." + :sql-query "SELECT url, title, last_visit_time/1000000-11644473600, visit_count FROM urls" + :file-path (prompt1 :prompt "Choose Vivaldi History file" + :sources (make-instance 'external-browser-history-source + :browser-lambda (lambda (subdir) + (when (and (str:starts-with-p "vivaldi" + (nfiles:basename (nfiles:parent subdir))) + (uiop:file-exists-p (uiop:merge-pathnames* "History" subdir))) + (uiop:merge-pathnames* "History" subdir))))))