Skip to content

Commit

Permalink
Improve search selected text functionality
Browse files Browse the repository at this point in the history
Also switch to python-based formatting for cdefs
  • Loading branch information
mufeedali committed Apr 23, 2024
1 parent f16202e commit 7316639
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 88 deletions.
109 changes: 53 additions & 56 deletions wordbook/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
from concurrent.futures import ThreadPoolExecutor
from functools import lru_cache
from shutil import rmtree
from typing import Callable, Dict, List

import wn
from wn import Wordnet
from wn import Form, Wordnet

from wordbook import utils

Expand All @@ -41,7 +42,7 @@ def wrap(*args, **kwargs):
return wrap


def cleaner(search_term):
def clean_search_terms(search_term):
"""Clean up search terms."""
text = search_term.strip().strip('<>"-?`![](){}/:;,*')
cleaner_list = ["(", ")", "<", ">", "[", "]", "&", "\\", "\n"]
Expand All @@ -50,12 +51,10 @@ def cleaner(search_term):
return text


def fold_gen():
def create_required_dirs():
"""Make required directories if they don't already exist."""
if not os.path.exists(utils.CONFIG_DIR): # check for Wordbook folder
os.makedirs(utils.CONFIG_DIR) # create Wordbook folder
if not os.path.exists(utils.CDEF_DIR): # check Custom Definitions folder.
os.makedirs(utils.CDEF_DIR) # create Custom Definitions folder.
os.makedirs(utils.CONFIG_DIR, exist_ok=True) # create Wordbook folder
os.makedirs(utils.CDEF_DIR, exist_ok=True) # create Custom Definitions folder.


def fetch_definition(text, wordcol, sencol, wn_instance, cdef=True, accent="us"):
Expand All @@ -68,43 +67,35 @@ def fetch_definition(text, wordcol, sencol, wn_instance, cdef=True, accent="us")
def get_cowfortune():
"""Present cowsay version of fortune easter egg."""
try:
cowsay = subprocess.Popen(
["cowsay", get_fortune(mono=False)],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cowsaid: str = (
subprocess.Popen(
["cowsay", get_fortune(mono=False)],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
.communicate()[0]
.decode()
)
cowsay.wait()
if cowsay:
cst = cowsay.stdout.read().decode()
return f"<tt>{html.escape(cst)}</tt>"
if cowsaid:
return f"<tt>{html.escape(cowsaid)}</tt>"
return "<tt>Cowsay fail… Too bad…</tt>"
except OSError as ex:
fortune_out = "Easter Egg Fail!!! Install 'fortune' or 'fortunemod' and also 'cowsay'."
print(f"{fortune_out}\n{str(ex)}")
return f"<tt>{fortune_out}</tt>"


def get_custom_def(text, wordcol, sencol, wn_instance, accent="us"):
def get_custom_def(text: str, wordcol: str, sencol: str, wn_instance, accent="us"):
"""Present custom definition when available."""
with open(f"{utils.CDEF_DIR}/{text}", "r") as def_file:
custom_def_dict = json.load(def_file)
custom_def_dict: dict = json.load(def_file)
if "linkto" in custom_def_dict:
return get_data(custom_def_dict.get("linkto", text), wordcol, sencol, wn_instance, accent)
definition = custom_def_dict.get(
"out_string",
get_definition(text, wordcol, sencol, wn_instance)[0]["out_string"],
)
re_list = {
"<i>($WORDCOL)</i>": wordcol,
"<i>($SENCOL)</i>": sencol,
"($WORDCOL)": wordcol,
"($SENCOL)": sencol,
"$WORDCOL": wordcol,
"$SENCOL": sencol,
}
if definition is not None:
for i, j in re_list.items():
definition = definition.replace(i, j)
definition = definition.format(WORDCOL=wordcol, SENCOL=sencol)
term = custom_def_dict.get("term", text)
pronunciation = custom_def_dict.get("pronunciation", get_pronunciation(term, accent)) or "Is espeak-ng installed?"
final_data = {
Expand All @@ -117,23 +108,27 @@ def get_custom_def(text, wordcol, sencol, wn_instance, accent="us"):

def get_data(term, word_col, sen_col, wn_instance, accent="us"):
"""Obtain the data to be processed and presented."""

# Obtain definition from given parameters
definition = get_definition(term, word_col, sen_col, wn_instance)
clean_def = definition[0]

# Get pronunciation of the term or default to the original term if no pronunciation available.
pron = get_pronunciation(clean_def["term"] or term, accent)
if not pron or pron == "" or pron.isspace():
final_pron = "Is espeak-ng installed?"
else:
final_pron = pron
final_pron = pron if pron and not pron.isspace() else "Is espeak-ng installed?"

# Create the dictionary to be returned.
final_data = {
"term": clean_def["term"],
"pronunciation": final_pron,
"result": clean_def["result"],
"out_string": clean_def["out_string"],
}

return final_data


def get_definition(term, word_col, sen_col, wn_instance):
def get_definition(term: str, word_col: str, sen_col: str, wn_instance):
"""Get the definition from python-wn and process it."""
result_dict = None
synsets = wn_instance.synsets(term) # Get relevant synsets.
Expand Down Expand Up @@ -232,33 +227,34 @@ def get_definition(term, word_col, sen_col, wn_instance):
def get_fortune(mono=True):
"""Present fortune easter egg."""
try:
fortune_process = subprocess.Popen(["fortune", "-a"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
fortune_process.wait()
fortune_out = fortune_process.stdout.read().decode()
fortune_out = html.escape(fortune_out, False)
fortune_output = (
subprocess.Popen(["fortune", "-a"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
.communicate()[0]
.decode()
)
if fortune_output:
fortune_output = html.escape(fortune_output, False)
except OSError as ex:
fortune_out = "Easter egg fail! Install 'fortune' or 'fortune-mod'."
utils.log_error(f"{fortune_out}\n{str(ex)}")
fortune_output = "Easter egg fail! Install 'fortune' or 'fortune-mod'."
utils.log_error(f"{fortune_output}\n{str(ex)}")
if mono:
return f"<tt>{fortune_out}</tt>"
return fortune_out
return f"<tt>{fortune_output}</tt>"
return fortune_output


@lru_cache(maxsize=128)
def get_pronunciation(term, accent="us"):
"""Get the pronunciation from espeak and process it."""
try:
process_pron = subprocess.Popen(
pron_output = (
subprocess.Popen(
["espeak-ng", "-v", f"en-{accent}", "--ipa", "-q", term],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except OSError as ex:
utils.log_warning(f"Didn't Work! Error: {str(ex)}")
return None
process_pron.wait()
output = process_pron.stdout.read().decode()
clean_output = " /{0}/".format(output.strip().replace("\n ", " "))
.communicate()[0]
.decode()
)
clean_output = " /{0}/".format(pron_output.strip().replace("\n ", " "))
return clean_output


Expand All @@ -268,20 +264,22 @@ def get_version_info(version):
print("Copyright 2016-2024 Mufeed Ali")
print()
try:
espeak_process = subprocess.Popen(["espeak-ng", "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
espeak_process.wait()
espeak_out = espeak_process.stdout.read().decode()
espeak_out = (
subprocess.Popen(["espeak-ng", "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
.communicate()[0]
.decode()
)
print(espeak_out.strip())
except OSError as ex:
print(f"You're missing a few dependencies. (espeak-ng)\n{str(ex)}")


@_threadpool
def get_wn_file(reloader):
def get_wn_file(reloader: Callable) -> Dict[str, Wordnet | List[Form]]:
"""Get the WordNet wordlist according to WordNet version."""
utils.log_info("Initializing WordNet.")
try:
wn_instance = Wordnet(lexicon=WN_DB_VERSION)
wn_instance: Wordnet = Wordnet(lexicon=WN_DB_VERSION)
except (wn.Error, wn.DatabaseError):
utils.log_info("The WordNet database is either corrupted or is of an older version.")
return reloader()
Expand All @@ -295,8 +293,7 @@ def format_output(text, dark_font, wn_instance, cdef, accent="us"):
"""Return appropriate definitions."""
if dark_font:
sencol = "cyan" # Color of sentences in Dark mode
wordcol = "lightgreen" # Color of: Similar words,
# Synonyms and Antonyms.
wordcol = "lightgreen" # Color of: Similar words, Synonyms and Antonyms.
else:
sencol = "blue" # Color of sentences in regular
wordcol = "green" # Color of: Similar words, Synonyms, Antonyms.
Expand Down
10 changes: 4 additions & 6 deletions wordbook/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# SPDX-FileCopyrightText: 2016-2024 Mufeed Ali <[email protected]>
# SPDX-License-Identifier: GPL-3.0-or-later

import gi

from gettext import gettext as _

import gi

gi.require_version("Gdk", "4.0")
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
Expand Down Expand Up @@ -65,12 +65,10 @@ def __init__(self, app_id, version):
)

Adw.StyleManager.get_default().set_color_scheme(
Adw.ColorScheme.FORCE_DARK
if Settings.get().gtk_dark_ui
else Adw.ColorScheme.PREFER_LIGHT
Adw.ColorScheme.FORCE_DARK if Settings.get().gtk_dark_ui else Adw.ColorScheme.PREFER_LIGHT
)

base.fold_gen()
base.create_required_dirs()

def do_startup(self):
"""Manage startup of the application."""
Expand Down
73 changes: 47 additions & 26 deletions wordbook/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class WordbookWindow(Adw.ApplicationWindow):
_search_queue = []
_last_search_fail = False
_active_thread = None
_primary_clipboard_text = None

def __init__(self, term="", **kwargs):
"""Initialize the window."""
Expand Down Expand Up @@ -131,41 +132,71 @@ def setup_widgets(self):
if not Settings.get().live_search:
self.set_default_widget(self.search_button)

def_extra_menu_model = Gio.Menu.new()
item = Gio.MenuItem.new("Search Selected Text", "win.search-selected")
def_extra_menu_model.append_item(item)

# Set the extra menu model for the label
self._def_view.set_extra_menu(def_extra_menu_model)

def setup_actions(self):
"""Setup the Gio actions for the application window."""
paste_search_action = Gio.SimpleAction.new("paste-search", None)
paste_search_action: Gio.SimpleAction = Gio.SimpleAction.new("paste-search", None)
paste_search_action.connect("activate", self.on_paste_search)
self.add_action(paste_search_action)

preferences_action = Gio.SimpleAction.new("preferences", None)
preferences_action: Gio.SimpleAction = Gio.SimpleAction.new("preferences", None)
preferences_action.connect("activate", self.on_preferences)
self.add_action(preferences_action)

random_word_action = Gio.SimpleAction.new("random-word", None)
random_word_action: Gio.SimpleAction = Gio.SimpleAction.new("random-word", None)
random_word_action.connect("activate", self.on_random_word)
self.add_action(random_word_action)

search_selected_action = Gio.SimpleAction.new("search-selected", None)
search_selected_action: Gio.SimpleAction = Gio.SimpleAction.new("search-selected", None)
search_selected_action.connect("activate", self.on_search_selected)
search_selected_action.set_enabled(False)
self.add_action(search_selected_action)

def_extra_menu_model = Gio.Menu.new()
item = Gio.MenuItem.new("Search Selected Text", "win.search-selected")
def_extra_menu_model.append_item(item)
clipboard: Gdk.Clipboard = self.get_primary_clipboard()
clipboard.connect("changed", self.on_clipboard_changed)

# Set the extra menu model for the label
self._def_view.set_extra_menu(def_extra_menu_model)
def on_clipboard_changed(self, clipboard: Gdk.Clipboard | None):
clipboard = Gdk.Display.get_default().get_primary_clipboard()

def on_selection(_clipboard, result):
"""Callback for the text selection."""
try:
text = clipboard.read_text_finish(result)
if text is not None and not text.strip() == "" and not text.isspace():
text = text.replace(" ", "").replace("\n", "")
self._primary_clipboard_text = text
self.lookup_action("search-selected").props.enabled = True
else:
self._primary_clipboard_text = None
self.lookup_action("search-selected").props.enabled = False
except GLib.GError:
# Usually happens when clipboard is empty or unsupported data type
self._primary_clipboard_text = None
self.lookup_action("search-selected").props.enabled = False

cancellable = Gio.Cancellable()
clipboard.read_text_async(cancellable, on_selection)

def on_paste_search(self, _action, _param):
"""Search text in clipboard."""
clipboard = Gdk.Display.get_default().get_clipboard()

def on_paste(_clipboard, result):
"""Callback for the clipboard paste."""
text = clipboard.read_text_finish(result)
text = base.cleaner(text)
if text is not None and not text.strip() == "" and not text.isspace():
self.trigger_search(text)
try:
text = clipboard.read_text_finish(result)
text = base.clean_search_terms(text)
if text is not None and not text.strip() == "" and not text.isspace():
self.trigger_search(text)
except GLib.GError:
# Usually happens when clipboard is empty or unsupported data type
pass

cancellable = Gio.Cancellable()
clipboard.read_text_async(cancellable, on_paste)
Expand All @@ -183,17 +214,7 @@ def on_random_word(self, _action, _param):

def on_search_selected(self, _action, _param):
"""Search selected text from inside or outside the window."""
clipboard = Gdk.Display.get_default().get_primary_clipboard()

def on_selection(_clipboard, result):
"""Callback for the text selection."""
text = clipboard.read_text_finish(result)
if text is not None and not text.strip() == "" and not text.isspace():
text = text.replace(" ", "").replace("\n", "")
self.trigger_search(text)

cancellable = Gio.Cancellable()
clipboard.read_text_async(cancellable, on_selection)
self.trigger_search(self._primary_clipboard_text)

def on_search_clicked(self, _button=None, pass_check=False, text=None):
"""Pass data to search function and set TextView data."""
Expand Down Expand Up @@ -311,7 +332,7 @@ def _on_def_stop_event(self, _click):

def on_paste(_clipboard, result):
text = clipboard.read_text_finish(result)
text = base.cleaner(text)
text = base.clean_search_terms(text)
if text is not None and not text.strip() == "" and not text.isspace():
self.trigger_search(text)

Expand Down Expand Up @@ -501,7 +522,7 @@ def _process_word_links(word_list, word_col):

def _search(self, search_text):
"""Clean input text, give errors and pass data to formatter."""
text = base.cleaner(search_text)
text = base.clean_search_terms(search_text)
if not text == "" and not text.isspace():
return base.format_output(
text,
Expand Down

0 comments on commit 7316639

Please sign in to comment.