trying qutebrowser again, tweaking scripts and sway keybinds
This commit is contained in:
@@ -0,0 +1,155 @@
|
|||||||
|
# If a config.py file exists, this file is ignored unless it's explicitly loaded
|
||||||
|
# via config.load_autoconfig(). For more information, see:
|
||||||
|
# https://github.com/qutebrowser/qutebrowser/blob/master/doc/help/configuring.asciidoc#loading-autoconfigyml
|
||||||
|
# DO NOT edit this file by hand, qutebrowser will overwrite it.
|
||||||
|
# Instead, create a config.py - see :help for details.
|
||||||
|
|
||||||
|
config_version: 2
|
||||||
|
settings:
|
||||||
|
content.blocking.method:
|
||||||
|
global: both
|
||||||
|
content.javascript.enabled:
|
||||||
|
'*://*.01.emailinboundprocessing.eu/*': true
|
||||||
|
'*://*.127.0.0.1/*': true
|
||||||
|
'*://*.4chanlit.fandom.com/*': true
|
||||||
|
'*://*.account.proton.me/*': true
|
||||||
|
'*://*.account.protonvpn.com/*': true
|
||||||
|
'*://*.ancientdead.com/*': true
|
||||||
|
'*://*.app.netdata.cloud/*': true
|
||||||
|
'*://*.app.simplelogin.io/*': true
|
||||||
|
'*://*.askubuntu.com/*': true
|
||||||
|
'*://*.babeground.com/*': true
|
||||||
|
'*://*.babeground.net/*': true
|
||||||
|
'*://*.babegrounds.live/*': true
|
||||||
|
'*://*.bandcamp.com/*': true
|
||||||
|
'*://*.chmodcommand.com/*': true
|
||||||
|
'*://*.codeberg.org/*': true
|
||||||
|
'*://*.confluence.atg-corp.com/*': false
|
||||||
|
'*://*.crontab.guru/*': true
|
||||||
|
'*://*.customer.xfinity.com/*': true
|
||||||
|
'*://*.demo.uptime.kuma.pet/*': true
|
||||||
|
'*://*.docs.google.com/*': true
|
||||||
|
'*://*.drop.download/*': true
|
||||||
|
'*://*.duckduckgo.com/*': true
|
||||||
|
'*://*.features.jellyfin.org/*': true
|
||||||
|
'*://*.flathub.org/*': true
|
||||||
|
'*://*.fleet.linuxserver.io/*': true
|
||||||
|
'*://*.fmovies.to/*': true
|
||||||
|
'*://*.framatube.org/*': true
|
||||||
|
'*://*.get.adobe.com/*': true
|
||||||
|
'*://*.github.com/*': true
|
||||||
|
'*://*.gitlab.com/*': true
|
||||||
|
'*://*.goauthentik.io/*': true
|
||||||
|
'*://*.greyzone.com/*': true
|
||||||
|
'*://*.heretic.camp/*': true
|
||||||
|
'*://*.html.duckduckgo.com/*': true
|
||||||
|
'*://*.hub.docker.com/*': true
|
||||||
|
'*://*.id.atlassian.com/*': true
|
||||||
|
'*://*.idcs-3359adb31e35415e8c1729c5c8098c6d.identity.oraclecloud.com/*': true
|
||||||
|
'*://*.imgflip.com/*': true
|
||||||
|
'*://*.inv.odyssey346.dev/*': true
|
||||||
|
'*://*.jf.opal.sh/*': true
|
||||||
|
'*://*.jira.atg-corp.com/*': true
|
||||||
|
'*://*.layoffs.fyi/*': true
|
||||||
|
'*://*.learningnetwork.cisco.com/*': true
|
||||||
|
'*://*.listen.20buckspin.com/*': true
|
||||||
|
'*://*.localhost/*': true
|
||||||
|
'*://*.login.seattle.gov/*': true
|
||||||
|
'*://*.login.xfinity.com/*': true
|
||||||
|
'*://*.looking-glass.io/*': true
|
||||||
|
'*://*.lotrproject.com/*': true
|
||||||
|
'*://*.mail.proton.me/*': true
|
||||||
|
'*://*.main.realsecure.flyingcroc.net/*': true
|
||||||
|
'*://*.metalodyssey.8merch.com/*': true
|
||||||
|
'*://*.my.uscis.gov/*': true
|
||||||
|
'*://*.my.vultr.com/*': true
|
||||||
|
'*://*.myaccount.standingstonegames.com/*': true
|
||||||
|
'*://*.myutilities.seattle.gov/*': true
|
||||||
|
'*://*.nagios.realsecure.flyingcroc.net/*': true
|
||||||
|
'*://*.netbox.realsecure.flyingcroc.net/*': true
|
||||||
|
'*://*.online.adp.com/*': true
|
||||||
|
'*://*.onlinebanking.becu.org/*': true
|
||||||
|
'*://*.onyxboox.com/*': true
|
||||||
|
'*://*.openscrobbler.com/*': true
|
||||||
|
'*://*.payments.xfinity.com/*': true
|
||||||
|
'*://*.portainer.opal.sh/*': true
|
||||||
|
'*://*.protesilaos.com/*': true
|
||||||
|
'*://*.proton.me/*': true
|
||||||
|
'*://*.rateyourmusic.com/*': true
|
||||||
|
'*://*.register.be.xfinity.com/*': false
|
||||||
|
'*://*.reverb.com/*': true
|
||||||
|
'*://*.sack.realsecure.flyingcroc.net/*': true
|
||||||
|
'*://*.search.brave.com/*': true
|
||||||
|
'*://*.seattle.bibliocommons.com/*': true
|
||||||
|
'*://*.seattle.craigslist.org/*': true
|
||||||
|
'*://*.shop.bulletproof.com/*': true
|
||||||
|
'*://*.spl.overdrive.com/*': true
|
||||||
|
'*://*.stackoverflow.com/*': true
|
||||||
|
'*://*.stash.atg-corp.com/*': true
|
||||||
|
'*://*.superuser.com/*': true
|
||||||
|
'*://*.support-acquia.force.com/*': true
|
||||||
|
'*://*.thecirclemusic.gr/*': true
|
||||||
|
'*://*.thegrayzone.com/*': true
|
||||||
|
'*://*.translate.google.com/*': true
|
||||||
|
'*://*.twitter.com/*': true
|
||||||
|
'*://*.unix.stackexchange.com/*': true
|
||||||
|
'*://*.uploadev.org/*': true
|
||||||
|
'*://*.utilities-self-service.ebill.seattle.gov/*': true
|
||||||
|
'*://*.veganmenshoes.com/*': true
|
||||||
|
'*://*.vid.puffyan.us/*': true
|
||||||
|
'*://*.wooddove.fciis.net/*': true
|
||||||
|
'*://*.www.20buckspin.com/*': true
|
||||||
|
'*://*.www.airplane.dev/*': true
|
||||||
|
'*://*.www.amazon.com/*': true
|
||||||
|
'*://*.www.astound.com/*': true
|
||||||
|
'*://*.www.budgetbytes.com/*': true
|
||||||
|
'*://*.www.calculator.net/*': true
|
||||||
|
'*://*.www.carhartt.com/*': true
|
||||||
|
'*://*.www.dancarlin.com/*': true
|
||||||
|
'*://*.www.drmartens.com/*': true
|
||||||
|
'*://*.www.duluthtrading.com/*': true
|
||||||
|
'*://*.www.easyime.com/*': true
|
||||||
|
'*://*.www.ebay.com/*': true
|
||||||
|
'*://*.www.filson.com/*': true
|
||||||
|
'*://*.www.foodnetwork.com/*': true
|
||||||
|
'*://*.www.geoguessr.com/*': true
|
||||||
|
'*://*.www.goodreads.com/*': true
|
||||||
|
'*://*.www.google.com/*': true
|
||||||
|
'*://*.www.harddrivesdirect.com/*': true
|
||||||
|
'*://*.www.knoll.com/*': true
|
||||||
|
'*://*.www.kobo.com/*': true
|
||||||
|
'*://*.www.last.fm/*': true
|
||||||
|
'*://*.www.lastpodcastontheleft.com/*': true
|
||||||
|
'*://*.www.lotro.com/*': true
|
||||||
|
'*://*.www.metal-archives.com/*': true
|
||||||
|
'*://*.www.mmorpg.com/*': true
|
||||||
|
'*://*.www.moddb.com/*': true
|
||||||
|
'*://*.www.netaddictionrecovery.com/*': true
|
||||||
|
'*://*.www.nightshiftmerch.com/*': true
|
||||||
|
'*://*.www.openstreetmap.org/*': true
|
||||||
|
'*://*.www.patagonia.com/*': true
|
||||||
|
'*://*.www.paypal.com/*': true
|
||||||
|
'*://*.www.raspberrypi.com/*': true
|
||||||
|
'*://*.www.reddit.com/*': true
|
||||||
|
'*://*.www.seattle.gov/*': true
|
||||||
|
'*://*.www.startpage.com/*': true
|
||||||
|
'*://*.www.thisisclassicalguitar.com/*': true
|
||||||
|
'*://*.www.vegetarian-shoes.co.uk/*': true
|
||||||
|
'*://*.www.vultr.com/*': true
|
||||||
|
'*://*.www.wills-vegan-store.com/*': true
|
||||||
|
'*://*.www.xfinity.com/*': true
|
||||||
|
'*://*.www.youtube.com/*': true
|
||||||
|
'*://*.www.zillow.com/*': true
|
||||||
|
'*://gethomepage.dev/*': true
|
||||||
|
'*://jf.opal.sh/*': true
|
||||||
|
'*://jsonformatter.curiousconcept.com/*': true
|
||||||
|
'*://rateyourmusic.com/*': true
|
||||||
|
'*://revelationofdoom.com/*': true
|
||||||
|
'*://www.duplicati.com/*': true
|
||||||
|
'*://www.newegg.com/*': true
|
||||||
|
'*://www.vitamix.com/*': true
|
||||||
|
'*://www.youtube.com/*': true
|
||||||
|
content.notifications.enabled:
|
||||||
|
https://jf.opal.sh: false
|
||||||
|
content.register_protocol_handler:
|
||||||
|
https://mail.proton.me#mailto=%25s: false
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from qutebrowser.api import interceptor
|
||||||
|
|
||||||
|
# Load existing settings made via :set
|
||||||
|
config.load_autoconfig()
|
||||||
|
|
||||||
|
|
||||||
|
c.qt.highdpi = True
|
||||||
|
c.fonts.default_size = "15pt"
|
||||||
|
c.fonts.tabs.selected = "13pt default_family"
|
||||||
|
c.fonts.tabs.unselected = "13pt default_family"
|
||||||
|
c.zoom.default = "130%"
|
||||||
|
c.content.javascript.enabled = False
|
||||||
|
c.downloads.location.directory = "~/downloads"
|
||||||
|
c.editor.command = ["alacritty", "-e", "nvim", "{}"]
|
||||||
|
c.editor.encoding = "utf-8"
|
||||||
|
c.auto_save.session = True
|
||||||
|
c.colors.webpage.preferred_color_scheme = "dark"
|
||||||
|
|
||||||
|
# privacy
|
||||||
|
c.content.cookies.accept = "no-3rdparty"
|
||||||
|
c.content.webrtc_ip_handling_policy = "default-public-interface-only"
|
||||||
|
c.content.site_specific_quirks.enabled = False
|
||||||
|
c.content.headers.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.79 Safari/537.36"
|
||||||
|
|
||||||
|
config.bind("j", "scroll-px 0 100")
|
||||||
|
config.bind("k", "scroll-px 0 -100")
|
||||||
|
config.bind("K", "tab-next")
|
||||||
|
config.bind("J", "tab-prev")
|
||||||
|
|
||||||
|
c.url.searchengines = {
|
||||||
|
"DEFAULT": "https://duckduckgo.com/?q={}",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.url.start_pages = ["https://start.duckduckgo.com"]
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
|||||||
|
[FileDialog]
|
||||||
|
history=file:///home/opal/pictures
|
||||||
|
lastVisited=file:///home/opal/pictures
|
||||||
|
qtVersion=5.15.6
|
||||||
|
shortcuts=file:, file:///home/opal
|
||||||
|
sidebarWidth=87
|
||||||
|
treeViewHeader="@ByteArray(\0\0\0\xff\0\0\0\0\0\0\0\x1\0\0\0\0\0\0\0\0\x1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x1\xd1\0\0\0\x4\x1\x1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x64\xff\xff\xff\xff\0\0\0\x81\0\0\0\0\0\0\0\x4\0\0\0\xf5\0\0\0\x1\0\0\0\0\0\0\0\x39\0\0\0\x1\0\0\0\0\0\0\0=\0\0\0\x1\0\0\0\0\0\0\0\x66\0\0\0\x1\0\0\0\0\0\0\x3\xe8\0\xff\xff\xff\xff)"
|
||||||
|
viewMode=Detail
|
||||||
File diff suppressed because it is too large
Load Diff
Executable
+343
@@ -0,0 +1,343 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||||
|
# Copyright 2014-2018 Claude (longneck) <longneck@scratchbook.ch>
|
||||||
|
|
||||||
|
# This file is part of qutebrowser.
|
||||||
|
#
|
||||||
|
# qutebrowser is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# qutebrowser is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
"""Tool to import data from other browsers.
|
||||||
|
|
||||||
|
Currently importing bookmarks from Netscape HTML Bookmark files, Chrome
|
||||||
|
profiles, and Mozilla profiles is supported.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import textwrap
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
import json
|
||||||
|
import string
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = get_args()
|
||||||
|
bookmark_types = []
|
||||||
|
output_format = None
|
||||||
|
input_format = args.input_format
|
||||||
|
if args.search_output:
|
||||||
|
bookmark_types = ["search"]
|
||||||
|
if args.oldconfig:
|
||||||
|
output_format = "oldsearch"
|
||||||
|
else:
|
||||||
|
output_format = "search"
|
||||||
|
else:
|
||||||
|
if args.bookmark_output:
|
||||||
|
output_format = "bookmark"
|
||||||
|
elif args.quickmark_output:
|
||||||
|
output_format = "quickmark"
|
||||||
|
if args.import_bookmarks:
|
||||||
|
bookmark_types.append("bookmark")
|
||||||
|
if args.import_keywords:
|
||||||
|
bookmark_types.append("keyword")
|
||||||
|
if not bookmark_types:
|
||||||
|
bookmark_types = ["bookmark", "keyword"]
|
||||||
|
if not output_format:
|
||||||
|
output_format = "quickmark"
|
||||||
|
|
||||||
|
import_function = {
|
||||||
|
"html": import_html_bookmarks,
|
||||||
|
"mozilla": import_moz_places,
|
||||||
|
"chrome": import_chrome,
|
||||||
|
}
|
||||||
|
import_function[input_format](args.bookmarks, bookmark_types, output_format)
|
||||||
|
|
||||||
|
|
||||||
|
def get_args():
|
||||||
|
"""Get the argparse parser."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog=textwrap.dedent(
|
||||||
|
"""
|
||||||
|
To import bookmarks, you'll need the path to your profile or an
|
||||||
|
exported HTML file from your browser's bookmark manager. Redirect
|
||||||
|
the output from this script to the appropriate file in your
|
||||||
|
qutebrowser config directory (listed in the output of :version),
|
||||||
|
usually done with the '>' operator; for example,
|
||||||
|
./importer.py -i mozilla your_profile_path > ~/.config/qutebrowser/quickmarks
|
||||||
|
|
||||||
|
Common browsers with native input format support:
|
||||||
|
chrome: Chrome, Chromium, Edge
|
||||||
|
mozilla: Firefox, SeaMonkey, Pale Moon
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-i",
|
||||||
|
"--input-format",
|
||||||
|
help="Which input format? Defaults to html",
|
||||||
|
choices=["html", "mozilla", "chrome"],
|
||||||
|
default="html",
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-b",
|
||||||
|
"--bookmark-output",
|
||||||
|
help="Output in bookmark format.",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-q",
|
||||||
|
"--quickmark-output",
|
||||||
|
help="Output in quickmark format (default).",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-s",
|
||||||
|
"--search-output",
|
||||||
|
help="Output config.py search engine format (negates -B and -K)",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--oldconfig",
|
||||||
|
help="Output search engine format for old qutebrowser.conf format",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-B",
|
||||||
|
"--import-bookmarks",
|
||||||
|
help="Import plain bookmarks (can be combiend with -K)",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-K",
|
||||||
|
"--import-keywords",
|
||||||
|
help="Import keywords (can be combined with -B)",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"bookmarks",
|
||||||
|
help="Bookmarks file (html format) or " "profile folder (Mozilla format)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def search_escape(url):
|
||||||
|
"""Escape URLs such that preexisting { and } are handled properly.
|
||||||
|
|
||||||
|
Will obviously trash a properly-formatted qutebrowser URL.
|
||||||
|
"""
|
||||||
|
return url.replace("{", "{{").replace("}", "}}")
|
||||||
|
|
||||||
|
|
||||||
|
def opensearch_convert(url):
|
||||||
|
"""Convert a basic OpenSearch URL into something qutebrowser can use.
|
||||||
|
|
||||||
|
Exceptions:
|
||||||
|
KeyError:
|
||||||
|
An unknown and required parameter is present in the URL. This
|
||||||
|
usually means there's browser/addon specific functionality needed
|
||||||
|
to build the URL (I'm looking at you and your browser, Google) that
|
||||||
|
obviously won't be present here.
|
||||||
|
"""
|
||||||
|
subst = {
|
||||||
|
"searchTerms": "%s", # for proper escaping later
|
||||||
|
"language": "*",
|
||||||
|
"inputEncoding": "UTF-8",
|
||||||
|
"outputEncoding": "UTF-8",
|
||||||
|
}
|
||||||
|
|
||||||
|
# remove optional parameters (even those we don't support)
|
||||||
|
for param in string.Formatter().parse(url):
|
||||||
|
if param[1]:
|
||||||
|
if param[1].endswith("?"):
|
||||||
|
url = url.replace("{" + param[1] + "}", "")
|
||||||
|
elif param[2] and param[2].endswith("?"):
|
||||||
|
url = url.replace("{" + param[1] + ":" + param[2] + "}", "")
|
||||||
|
return search_escape(url.format(**subst)).replace("%s", "{}")
|
||||||
|
|
||||||
|
|
||||||
|
def import_html_bookmarks(bookmarks_file, bookmark_types, output_format):
|
||||||
|
"""Import bookmarks from a NETSCAPE-Bookmark-file v1.
|
||||||
|
|
||||||
|
Generated by Chromium, Firefox, IE and possibly more browsers. Not all
|
||||||
|
export all possible bookmark types:
|
||||||
|
- Firefox mostly works with everything
|
||||||
|
- Chrome doesn't support keywords at all; searches are a separate
|
||||||
|
database
|
||||||
|
"""
|
||||||
|
import bs4
|
||||||
|
|
||||||
|
with open(bookmarks_file, encoding="utf-8") as f:
|
||||||
|
soup = bs4.BeautifulSoup(f, "html.parser")
|
||||||
|
bookmark_query = {
|
||||||
|
"search": lambda tag: (
|
||||||
|
(tag.name == "a") and ("shortcuturl" in tag.attrs) and ("%s" in tag["href"])
|
||||||
|
),
|
||||||
|
"keyword": lambda tag: (
|
||||||
|
(tag.name == "a")
|
||||||
|
and ("shortcuturl" in tag.attrs)
|
||||||
|
and ("%s" not in tag["href"])
|
||||||
|
),
|
||||||
|
"bookmark": lambda tag: (
|
||||||
|
(tag.name == "a") and ("shortcuturl" not in tag.attrs) and (tag.string)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
output_template = {
|
||||||
|
"search": {
|
||||||
|
"search": "c.url.searchengines['{tag[shortcuturl]}'] = "
|
||||||
|
"'{tag[href]}' #{tag.string}"
|
||||||
|
},
|
||||||
|
"oldsearch": {
|
||||||
|
"search": "{tag[shortcuturl]} = {tag[href]} #{tag.string}",
|
||||||
|
},
|
||||||
|
"bookmark": {
|
||||||
|
"bookmark": "{tag[href]} {tag.string}",
|
||||||
|
"keyword": "{tag[href]} {tag.string}",
|
||||||
|
},
|
||||||
|
"quickmark": {
|
||||||
|
"bookmark": "{tag.string} {tag[href]}",
|
||||||
|
"keyword": "{tag[shortcuturl]} {tag[href]}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bookmarks = []
|
||||||
|
for typ in bookmark_types:
|
||||||
|
tags = soup.findAll(bookmark_query[typ])
|
||||||
|
for tag in tags:
|
||||||
|
if typ == "search":
|
||||||
|
tag["href"] = search_escape(tag["href"]).replace("%s", "{}")
|
||||||
|
if tag["href"] not in bookmarks:
|
||||||
|
bookmarks.append(output_template[output_format][typ].format(tag=tag))
|
||||||
|
for bookmark in bookmarks:
|
||||||
|
print(bookmark)
|
||||||
|
|
||||||
|
|
||||||
|
def import_moz_places(profile, bookmark_types, output_format):
|
||||||
|
"""Import bookmarks from a Mozilla profile's places.sqlite database."""
|
||||||
|
place_query = {
|
||||||
|
"bookmark": (
|
||||||
|
"SELECT DISTINCT moz_bookmarks.title,moz_places.url "
|
||||||
|
"FROM moz_bookmarks,moz_places "
|
||||||
|
"WHERE moz_places.id=moz_bookmarks.fk "
|
||||||
|
"AND moz_places.id NOT IN (SELECT place_id FROM moz_keywords) "
|
||||||
|
"AND moz_places.url NOT LIKE 'place:%';"
|
||||||
|
), # Bookmarks with no keywords assigned
|
||||||
|
"keyword": (
|
||||||
|
"SELECT moz_keywords.keyword,moz_places.url "
|
||||||
|
"FROM moz_keywords,moz_places,moz_bookmarks "
|
||||||
|
"WHERE moz_places.id=moz_bookmarks.fk "
|
||||||
|
"AND moz_places.id=moz_keywords.place_id "
|
||||||
|
"AND moz_places.url NOT LIKE '%!%s%' ESCAPE '!';"
|
||||||
|
), # Bookmarks with keywords assigned but no %s substitution
|
||||||
|
"search": (
|
||||||
|
"SELECT moz_keywords.keyword, "
|
||||||
|
" moz_bookmarks.title, "
|
||||||
|
" search_conv(moz_places.url) AS url "
|
||||||
|
"FROM moz_keywords,moz_places,moz_bookmarks "
|
||||||
|
"WHERE moz_places.id=moz_bookmarks.fk "
|
||||||
|
"AND moz_places.id=moz_keywords.place_id "
|
||||||
|
"AND moz_places.url LIKE '%!%s%' ESCAPE '!';"
|
||||||
|
), # bookmarks with keyword and %s substitution
|
||||||
|
}
|
||||||
|
out_template = {
|
||||||
|
"bookmark": {"bookmark": "{url} {title}", "keyword": "{url} {keyword}"},
|
||||||
|
"quickmark": {"bookmark": "{title} {url}", "keyword": "{keyword} {url}"},
|
||||||
|
"oldsearch": {"search": "{keyword} {url} #{title}"},
|
||||||
|
"search": {"search": "c.url.searchengines['{keyword}'] = '{url}' #{title}"},
|
||||||
|
}
|
||||||
|
|
||||||
|
def search_conv(url):
|
||||||
|
return search_escape(url).replace("%s", "{}")
|
||||||
|
|
||||||
|
places = sqlite3.connect(os.path.join(profile, "places.sqlite"))
|
||||||
|
places.create_function("search_conv", 1, search_conv)
|
||||||
|
places.row_factory = sqlite3.Row
|
||||||
|
c = places.cursor()
|
||||||
|
for typ in bookmark_types:
|
||||||
|
c.execute(place_query[typ])
|
||||||
|
for row in c:
|
||||||
|
print(out_template[output_format][typ].format(**row))
|
||||||
|
|
||||||
|
|
||||||
|
def import_chrome(profile, bookmark_types, output_format):
|
||||||
|
"""Import bookmarks and search keywords from Chrome-type profiles.
|
||||||
|
|
||||||
|
On Chrome, keywords and search engines are the same thing and handled in
|
||||||
|
their own database table; bookmarks cannot have associated keywords. This
|
||||||
|
is why the dictionary lookups here are much simpler.
|
||||||
|
"""
|
||||||
|
out_template = {
|
||||||
|
"bookmark": "{url} {name}",
|
||||||
|
"quickmark": "{name} {url}",
|
||||||
|
"search": "c.url.searchengines['{keyword}'] = '{url}'",
|
||||||
|
"oldsearch": "{keyword} {url}",
|
||||||
|
}
|
||||||
|
|
||||||
|
if "search" in bookmark_types:
|
||||||
|
webdata = sqlite3.connect(os.path.join(profile, "Web Data"))
|
||||||
|
c = webdata.cursor()
|
||||||
|
c.execute("SELECT keyword,url FROM keywords;")
|
||||||
|
for keyword, url in c:
|
||||||
|
try:
|
||||||
|
url = opensearch_convert(url)
|
||||||
|
print(out_template[output_format].format(keyword=keyword, url=url))
|
||||||
|
except KeyError:
|
||||||
|
print(
|
||||||
|
"# Unsupported parameter in url for {}; skipping....".format(
|
||||||
|
keyword
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
with open(os.path.join(profile, "Bookmarks"), encoding="utf-8") as f:
|
||||||
|
bookmarks = json.load(f)
|
||||||
|
|
||||||
|
def bm_tree_walk(bm, template):
|
||||||
|
"""Recursive function to walk through bookmarks."""
|
||||||
|
if not isinstance(bm, dict):
|
||||||
|
return
|
||||||
|
assert "type" in bm, bm
|
||||||
|
if bm["type"] == "url":
|
||||||
|
if urllib.parse.urlparse(bm["url"]).scheme != "chrome":
|
||||||
|
print(template.format(**bm))
|
||||||
|
elif bm["type"] == "folder":
|
||||||
|
for child in bm["children"]:
|
||||||
|
bm_tree_walk(child, template)
|
||||||
|
|
||||||
|
for root in bookmarks["roots"].values():
|
||||||
|
bm_tree_walk(root, out_template[output_format])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+356
@@ -0,0 +1,356 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright 2017 Chris Braun (cryzed) <cryzed@googlemail.com>
|
||||||
|
# Adapted for Bitwarden by Jonathan Haylett (JonnyHaystack) <jonathan@haylett.dev>
|
||||||
|
#
|
||||||
|
# This file is part of qutebrowser.
|
||||||
|
#
|
||||||
|
# qutebrowser is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# qutebrowser is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Insert login information using Bitwarden CLI and a dmenu-compatible application
|
||||||
|
(e.g. dmenu, rofi -dmenu, ...).
|
||||||
|
"""
|
||||||
|
|
||||||
|
USAGE = """The domain of the site has to be in the name of the Bitwarden entry, for example: "github.com/cryzed" or
|
||||||
|
"websites/github.com". The login information is inserted by emulating key events using qutebrowser's fake-key command in this manner:
|
||||||
|
[USERNAME]<Tab>[PASSWORD], which is compatible with almost all login forms.
|
||||||
|
|
||||||
|
If enabled, with the `--totp` flag, it will also move the TOTP code to the
|
||||||
|
clipboard, much like the Firefox add-on.
|
||||||
|
|
||||||
|
You must log into Bitwarden CLI using `bw login` prior to use of this script.
|
||||||
|
The session key will be stored using keyctl for the number of seconds passed to
|
||||||
|
the --auto-lock option.
|
||||||
|
|
||||||
|
To use in qutebrowser, run: `spawn --userscript qute-bitwarden`
|
||||||
|
"""
|
||||||
|
|
||||||
|
EPILOG = """Dependencies: tldextract (Python 3 module), pyperclip (optional
|
||||||
|
Python module, used for TOTP codes), Bitwarden CLI (1.7.4 is known to work
|
||||||
|
but older versions may well also work)
|
||||||
|
|
||||||
|
WARNING: The login details are viewable as plaintext in qutebrowser's debug log
|
||||||
|
(qute://log) and might be shared if you decide to submit a crash report!"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import enum
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import tldextract
|
||||||
|
|
||||||
|
argument_parser = argparse.ArgumentParser(
|
||||||
|
description=__doc__,
|
||||||
|
usage=USAGE,
|
||||||
|
epilog=EPILOG,
|
||||||
|
)
|
||||||
|
argument_parser.add_argument("url", nargs="?", default=os.getenv("QUTE_URL"))
|
||||||
|
argument_parser.add_argument(
|
||||||
|
"--dmenu-invocation",
|
||||||
|
"-d",
|
||||||
|
default="bemenu -x -b --prompt=Pass",
|
||||||
|
help="Invocation used to execute a dmenu-provider",
|
||||||
|
)
|
||||||
|
argument_parser.add_argument(
|
||||||
|
"--password-prompt-invocation",
|
||||||
|
"-p",
|
||||||
|
default='bemenu -x -b -p "Master Password"',
|
||||||
|
help="Invocation used to prompt the user for their Bitwarden password",
|
||||||
|
)
|
||||||
|
argument_parser.add_argument(
|
||||||
|
"--no-insert-mode",
|
||||||
|
"-n",
|
||||||
|
dest="insert_mode",
|
||||||
|
action="store_false",
|
||||||
|
help="Don't automatically enter insert mode",
|
||||||
|
)
|
||||||
|
argument_parser.add_argument(
|
||||||
|
"--totp", "-t", action="store_true", help="Copy TOTP key to clipboard"
|
||||||
|
)
|
||||||
|
argument_parser.add_argument(
|
||||||
|
"--io-encoding",
|
||||||
|
"-i",
|
||||||
|
default="UTF-8",
|
||||||
|
help="Encoding used to communicate with subprocesses",
|
||||||
|
)
|
||||||
|
argument_parser.add_argument(
|
||||||
|
"--merge-candidates",
|
||||||
|
"-m",
|
||||||
|
action="store_true",
|
||||||
|
help="Merge pass candidates for fully-qualified and registered domain name",
|
||||||
|
)
|
||||||
|
argument_parser.add_argument(
|
||||||
|
"--auto-lock",
|
||||||
|
type=int,
|
||||||
|
default=900,
|
||||||
|
help="Automatically lock the vault after this many seconds",
|
||||||
|
)
|
||||||
|
|
||||||
|
group = argument_parser.add_mutually_exclusive_group()
|
||||||
|
group.add_argument(
|
||||||
|
"--username-only", "-e", action="store_true", help="Only insert username"
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--password-only", "-w", action="store_true", help="Only insert password"
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--totp-only", "-T", action="store_true", help="Only insert totp code"
|
||||||
|
)
|
||||||
|
|
||||||
|
stderr = functools.partial(print, file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
class ExitCodes(enum.IntEnum):
|
||||||
|
SUCCESS = 0
|
||||||
|
FAILURE = 1
|
||||||
|
# 1 is automatically used if Python throws an exception
|
||||||
|
NO_PASS_CANDIDATES = 2
|
||||||
|
COULD_NOT_MATCH_USERNAME = 3
|
||||||
|
COULD_NOT_MATCH_PASSWORD = 4
|
||||||
|
|
||||||
|
|
||||||
|
def qute_command(command):
|
||||||
|
with open(os.environ["QUTE_FIFO"], "w") as fifo:
|
||||||
|
fifo.write(command + "\n")
|
||||||
|
fifo.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def ask_password(password_prompt_invocation):
|
||||||
|
process = subprocess.run(
|
||||||
|
shlex.split(password_prompt_invocation),
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
if process.returncode > 0:
|
||||||
|
raise Exception("Could not unlock vault")
|
||||||
|
master_pass = process.stdout.strip()
|
||||||
|
return subprocess.check_output(
|
||||||
|
["bw", "unlock", "--raw", master_pass],
|
||||||
|
text=True,
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def get_session_key(auto_lock, password_prompt_invocation):
|
||||||
|
if auto_lock == 0:
|
||||||
|
subprocess.call(["keyctl", "purge", "user", "bw_session"])
|
||||||
|
return ask_password(password_prompt_invocation)
|
||||||
|
else:
|
||||||
|
process = subprocess.run(
|
||||||
|
["keyctl", "request", "user", "bw_session"],
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
key_id = process.stdout.strip()
|
||||||
|
if process.returncode > 0:
|
||||||
|
session = ask_password(password_prompt_invocation)
|
||||||
|
if not session:
|
||||||
|
raise Exception("Could not unlock vault")
|
||||||
|
key_id = subprocess.check_output(
|
||||||
|
["keyctl", "add", "user", "bw_session", session, "@u"],
|
||||||
|
text=True,
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
if auto_lock > 0:
|
||||||
|
subprocess.call(["keyctl", "timeout", str(key_id), str(auto_lock)])
|
||||||
|
return subprocess.check_output(
|
||||||
|
["keyctl", "pipe", str(key_id)],
|
||||||
|
text=True,
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def pass_(domain, encoding, auto_lock, password_prompt_invocation):
|
||||||
|
session_key = get_session_key(auto_lock, password_prompt_invocation)
|
||||||
|
process = subprocess.run(
|
||||||
|
["bw", "list", "items", "--session", session_key, "--url", domain],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
err = process.stderr.decode(encoding).strip()
|
||||||
|
if err:
|
||||||
|
msg = "Bitwarden CLI returned for {:s} - {:s}".format(domain, err)
|
||||||
|
stderr(msg)
|
||||||
|
|
||||||
|
if process.returncode:
|
||||||
|
return "[]"
|
||||||
|
|
||||||
|
out = process.stdout.decode(encoding).strip()
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def get_totp_code(
|
||||||
|
selection_id, domain_name, encoding, auto_lock, password_prompt_invocation
|
||||||
|
):
|
||||||
|
session_key = get_session_key(auto_lock, password_prompt_invocation)
|
||||||
|
process = subprocess.run(
|
||||||
|
["bw", "get", "totp", "--session", session_key, selection_id],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
err = process.stderr.decode(encoding).strip()
|
||||||
|
if err:
|
||||||
|
# domain_name instead of selection_id to make it more user-friendly
|
||||||
|
msg = "Bitwarden CLI returned for {:s} - {:s}".format(domain_name, err)
|
||||||
|
stderr(msg)
|
||||||
|
|
||||||
|
if process.returncode:
|
||||||
|
return "[]"
|
||||||
|
|
||||||
|
out = process.stdout.decode(encoding).strip()
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def dmenu(items, invocation, encoding):
|
||||||
|
command = shlex.split(invocation)
|
||||||
|
process = subprocess.run(
|
||||||
|
command, input="\n".join(items).encode(encoding), stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
return process.stdout.decode(encoding).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def fake_key_raw(text):
|
||||||
|
for character in text:
|
||||||
|
# Escape all characters by default, space requires special handling
|
||||||
|
sequence = '" "' if character == " " else r"\{}".format(character)
|
||||||
|
qute_command("fake-key {}".format(sequence))
|
||||||
|
|
||||||
|
|
||||||
|
def main(arguments):
|
||||||
|
if not arguments.url:
|
||||||
|
argument_parser.print_help()
|
||||||
|
return ExitCodes.FAILURE
|
||||||
|
|
||||||
|
extract_result = tldextract.extract(arguments.url)
|
||||||
|
|
||||||
|
# Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains),
|
||||||
|
# the registered domain name and finally: the IPv4 address if that's what
|
||||||
|
# the URL represents
|
||||||
|
candidates = []
|
||||||
|
for target in filter(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
extract_result.fqdn,
|
||||||
|
extract_result.registered_domain,
|
||||||
|
extract_result.subdomain + "." + extract_result.domain,
|
||||||
|
extract_result.domain,
|
||||||
|
extract_result.ipv4,
|
||||||
|
],
|
||||||
|
):
|
||||||
|
target_candidates = json.loads(
|
||||||
|
pass_(
|
||||||
|
target,
|
||||||
|
arguments.io_encoding,
|
||||||
|
arguments.auto_lock,
|
||||||
|
arguments.password_prompt_invocation,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not target_candidates:
|
||||||
|
continue
|
||||||
|
|
||||||
|
candidates = candidates + target_candidates
|
||||||
|
if not arguments.merge_candidates:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if not candidates:
|
||||||
|
stderr("No pass candidates for URL {!r} found!".format(arguments.url))
|
||||||
|
return ExitCodes.NO_PASS_CANDIDATES
|
||||||
|
|
||||||
|
if len(candidates) == 1:
|
||||||
|
selection = candidates.pop()
|
||||||
|
else:
|
||||||
|
choices = []
|
||||||
|
for c in candidates:
|
||||||
|
if c["login"]["username"] is None:
|
||||||
|
c["login"]["username"] = ""
|
||||||
|
|
||||||
|
choices.append("{:s} | {:s}".format(c["name"], c["login"]["username"]))
|
||||||
|
|
||||||
|
# choices = [
|
||||||
|
# "{:s} | {:s}".format(c["name"], c["login"]["username"]) for c in candidates
|
||||||
|
# ]
|
||||||
|
choice = dmenu(choices, arguments.dmenu_invocation, arguments.io_encoding)
|
||||||
|
choice_tokens = choice.split("|")
|
||||||
|
choice_name = choice_tokens[0].strip()
|
||||||
|
choice_username = choice_tokens[1].strip()
|
||||||
|
selection = next(
|
||||||
|
(
|
||||||
|
c
|
||||||
|
for (i, c) in enumerate(candidates)
|
||||||
|
if c["name"] == choice_name
|
||||||
|
and c["login"]["username"] == choice_username
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Nothing was selected, simply return
|
||||||
|
if not selection:
|
||||||
|
return ExitCodes.SUCCESS
|
||||||
|
|
||||||
|
username = selection["login"]["username"]
|
||||||
|
password = selection["login"]["password"]
|
||||||
|
totp = selection["login"]["totp"]
|
||||||
|
|
||||||
|
if arguments.username_only:
|
||||||
|
fake_key_raw(username)
|
||||||
|
elif arguments.password_only:
|
||||||
|
fake_key_raw(password)
|
||||||
|
elif arguments.totp_only:
|
||||||
|
# No point in moving it to the clipboard in this case
|
||||||
|
fake_key_raw(
|
||||||
|
get_totp_code(
|
||||||
|
selection["id"],
|
||||||
|
selection["name"],
|
||||||
|
arguments.io_encoding,
|
||||||
|
arguments.auto_lock,
|
||||||
|
arguments.password_prompt_invocation,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
|
||||||
|
# back into insert-mode, so the form can be directly submitted by
|
||||||
|
# hitting enter afterwards
|
||||||
|
fake_key_raw(username)
|
||||||
|
qute_command("fake-key <Tab>")
|
||||||
|
fake_key_raw(password)
|
||||||
|
|
||||||
|
if arguments.insert_mode:
|
||||||
|
qute_command("mode-enter insert")
|
||||||
|
|
||||||
|
# If it finds a TOTP code, it copies it to the clipboard,
|
||||||
|
# which is the same behavior as the Firefox add-on.
|
||||||
|
if not arguments.totp_only and totp and arguments.totp:
|
||||||
|
# The import is done here, to make pyperclip an optional dependency
|
||||||
|
import pyperclip
|
||||||
|
|
||||||
|
pyperclip.copy(
|
||||||
|
get_totp_code(
|
||||||
|
selection["id"],
|
||||||
|
selection["name"],
|
||||||
|
arguments.io_encoding,
|
||||||
|
arguments.auto_lock,
|
||||||
|
arguments.password_prompt_invocation,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ExitCodes.SUCCESS
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
arguments = argument_parser.parse_args()
|
||||||
|
sys.exit(main(arguments))
|
||||||
@@ -163,8 +163,10 @@ bindsym $mod+Ctrl+g exec grimshot save window
|
|||||||
#bindsym $mod+Shift+e exec wlogout -p layer-shell
|
#bindsym $mod+Shift+e exec wlogout -p layer-shell
|
||||||
|
|
||||||
# Bookmarks
|
# Bookmarks
|
||||||
|
# Enter selected bookmark into target text field
|
||||||
bindsym $mod+i exec wtype $(grep -v '^#' ~/.local/share/bookmarks/bookmarks | bemenu -p Bookmark: -l 50 -c -M 500 --fn 'Monospace 14' --tf '#ff4e00' --ff '#dbc077' --hf '#ff4e00' | cut -d ' ' -f1)
|
bindsym $mod+i exec wtype $(grep -v '^#' ~/.local/share/bookmarks/bookmarks | bemenu -p Bookmark: -l 50 -c -M 500 --fn 'Monospace 14' --tf '#ff4e00' --ff '#dbc077' --hf '#ff4e00' | cut -d ' ' -f1)
|
||||||
|
|
||||||
|
# Insert bookmark into bookmark file (~/.local/share/bookmarks/bookmarks)
|
||||||
bindsym $mod+Shift+i exec ~/.local/bin/bookmark_insert
|
bindsym $mod+Shift+i exec ~/.local/bin/bookmark_insert
|
||||||
|
|
||||||
# Emojis
|
# Emojis
|
||||||
|
|||||||
@@ -9,20 +9,24 @@ if [ ! -f "$bookmarks_file" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Trim leading and trailing whitespace from clipboard
|
# Trim leading/trailing whitespace
|
||||||
clipboard_text=$(echo "$clipboard_text" | xargs)
|
clipboard_text=$(echo "$clipboard_text" | xargs)
|
||||||
|
|
||||||
|
# Strip https:// or http://
|
||||||
|
clipboard_text=$(echo "$clipboard_text" | sed 's#^https\?://##')
|
||||||
|
|
||||||
|
# Remove trailing slashes
|
||||||
|
clipboard_text=$(echo "$clipboard_text" | sed 's#/$##')
|
||||||
|
|
||||||
# Check if clipboard is not empty
|
# Check if clipboard is not empty
|
||||||
if [ -n "$clipboard_text" ]; then
|
if [ -n "$clipboard_text" ]; then
|
||||||
# Escape special characters for grep and check for exact match
|
|
||||||
if grep -Fxq "$clipboard_text" "$bookmarks_file"; then
|
if grep -Fxq "$clipboard_text" "$bookmarks_file"; then
|
||||||
notify-send "Bookmark already exists!" "$clipboard_text"
|
notify-send "Bookmark already exists!" "$clipboard_text"
|
||||||
else
|
else
|
||||||
# Append the text to the bookmarks file
|
|
||||||
echo "$clipboard_text" >> "$bookmarks_file"
|
echo "$clipboard_text" >> "$bookmarks_file"
|
||||||
notify-send "Bookmark saved" "$clipboard_text"
|
notify-send "Bookmark saved" "$clipboard_text"
|
||||||
|
|
||||||
# Run git update script
|
# Run git update script
|
||||||
sh "$HOME/.local/bin/bookmark_update"
|
sh "$HOME/.local/bin/bookmark_update"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user