diff options
author | Silvio Rhatto <rhatto@riseup.net> | 2024-07-24 09:21:10 -0300 |
---|---|---|
committer | Silvio Rhatto <rhatto@riseup.net> | 2024-07-24 09:21:10 -0300 |
commit | 07f236a571db25e0449d659e589ac5398776cee1 (patch) | |
tree | 1d82d436faaed43f123cee45eb62b21dac65f859 /packages | |
download | finder-07f236a571db25e0449d659e589ac5398776cee1.tar.gz finder-07f236a571db25e0449d659e589ac5398776cee1.tar.bz2 |
Initial import
Diffstat (limited to 'packages')
-rw-r--r-- | packages/finder/__init__.py | 0 | ||||
-rw-r--r-- | packages/finder/app.py | 128 | ||||
-rw-r--r-- | packages/finder/config.py | 137 | ||||
-rw-r--r-- | packages/finder/init.py | 77 | ||||
-rw-r--r-- | packages/finder/main.py | 207 | ||||
-rw-r--r-- | packages/finder/plugin/opener/file/xdg.py | 28 | ||||
-rw-r--r-- | packages/finder/plugin/searcher/file/name.py | 57 |
7 files changed, 634 insertions, 0 deletions
diff --git a/packages/finder/__init__.py b/packages/finder/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/packages/finder/__init__.py diff --git a/packages/finder/app.py b/packages/finder/app.py new file mode 100644 index 0000000..6efa29f --- /dev/null +++ b/packages/finder/app.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Finder searcher and browser REPL. +# +# Copyright (C) 2024 Silvio Rhatto <rhatto@riseup.net> +# +# This program 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 any later version. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +# Dependencies +import sys +import os +import signal + +try: + from .init import FinderInit + from .config import FinderConfig + from .main import FinderMain +except ImportError: + exit(1) + +class Finder( + FinderConfig, + FinderInit, + FinderMain, + ): + """ + Finder searcher and browser REPL. + + Like ranger, but with a search-like interface. + + Initially adapted from https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1109#issuecomment-606854044 + """ + +def finish(status=0): + """ + Stops Finder + + :type status: int + :param status: Exit status code. + """ + + try: + sys.exit(status) + except SystemExit: + os._exit(status) + +def finish_handler(signal, frame): + """ + Wrapper around finish() for handling system signals + + :type signal: int + :param signal: Signal number. + + :type frame: object + :param frame: Current stack frame. + """ + + print('Signal received, stopping Finder..') + + finish(1) + +def run(args): + """ + Run Finder from arguments + + :type args: dict + :param args: Instance arguments. + """ + + # Register signal handling + #signal.signal(signal.SIGINT, finish_handler) + signal.signal(signal.SIGTERM, finish_handler) + + # Exit status (shell convention means 0 is success, failure otherwise) + status = 0 + + # Dispatch + try: + finder = Finder(args) + + if finder.initialize() is not False: + status = 0 if finder.run() else 1 + else: + status = 1 + + print('Error: could not initialize') + + # Handle user interruption + # See https://stackoverflow.com/questions/21120947/catching-keyboardinterrupt-in-python-during-program-shutdown + except KeyboardInterrupt as e: + finder.log('Stopping Finder due to user request...') + + except FileNotFoundError as e: + status = 1 + + print('File not found: ' + str(e)) + + except Exception as e: + status = 1 + + print(repr(e)) + + finally: + #if 'finder' in locals(): + # finder.finish() + + finish(status) + +def run_from_cmdline(): + """ + Run Finder getting arguments from the command line. + """ + + from .config import cmdline + + run(cmdline()) diff --git a/packages/finder/config.py b/packages/finder/config.py new file mode 100644 index 0000000..6f24ffe --- /dev/null +++ b/packages/finder/config.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Finder searcher and browser REPL. +# +# Copyright (C) 2024 Silvio Rhatto <rhatto@riseup.net> +# +# This program 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 any later version. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +# Dependencies +import os +import argparse + +# The Finder version string +# Uses Semantic Versioning 2.0.0 +# See https://semver.org +finder_version = '0.0.1' + +# The base path for this project +basepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir) + os.sep + +# Describe configuration options +config = { + #'someoption': { + # 'help' : 'Some option help text', + # 'default' : 'default_value', + # 'action' : 'store', + # }, + } + +def cmdline_parser(): + """ + Generate command line arguments + + :rtype: argparse.ArgumentParser + :return: The parser object + """ + + epilog = """Examples: + + finder mydocs/ + """ + + description = 'REPL searcher and browser' + parser = argparse.ArgumentParser( + prog = 'finder', + description = description, + epilog = epilog, + formatter_class = argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument('path', help=""" + Path to look for items. Defaults to the current working directory.""".strip(), + nargs='?', + default='.') + + parser.add_argument('--config', help=""" + Read options from configuration file. All command line + parameters can be specified inside a YAML file. + Additional command line parameters override those set + in the configuration file.""".strip()) + + parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + finder_version) + + for argument in sorted(config): + config[argument]['type'] = type(config[argument]['default']) + + if not isinstance(config[argument]['default'], bool) and config[argument]['default'] != '': + config[argument]['help'] += ' (default: %(default)s)' + + parser.add_argument('--' + argument, **config[argument]) + + return parser + +def cmdline(): + """ + Evalutate the command line. + + :rtype: argparse.Namespace + :return: Command line arguments. + """ + + parser = cmdline_parser() + args = parser.parse_args() + + if args.path is None: + parser.print_usage() + exit(1) + + return args + +class FinderConfig: + """ + Finder class with configuration-related methods. + """ + + def get_config(self, item, default = None): + """ + Helper to get instance configuration + + Retrieve a config parameter from the self.config object or use a + default value as fallback + + :type item: str + :param item: Configuration item name + + :param default: Default config value to be used as a fallback if there's + no self.config[item] available. + Defaults to None + + :return: The configuration parameter value or the default fallback value. + """ + + if self.config is None: + self.config = {} + + if item in self.config: + return self.config[item] + + # Optionally override the default with an argument provided + elif default is not None: + self.config[item] = default + + return default + + return config[item]['default'] diff --git a/packages/finder/init.py b/packages/finder/init.py new file mode 100644 index 0000000..e0338da --- /dev/null +++ b/packages/finder/init.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Finder searcher and browser REPL. +# +# Copyright (C) 2024 Silvio Rhatto <rhatto@riseup.net> +# +# This program 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 any later version. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +# Dependencies +import os + +try: + import yaml +except ImportError: + print("Please install pyaml first!") + raise ImportError + +class FinderInit(): + # + # Initialization logic + # + + def __init__(self, args): + """ + Finder class constructor. + + Setup instance configuration. + + Handles command-line parameters. + + :type args: dict + :param args: Instance arguments. + """ + + self.args = args + self.data = [] + + # Environment variable handling + if 'FINDER_CONFIG' in os.environ and os.environ['FINDER_CONFIG'] != '': + args.config = os.environ['FINDER_CONFIG'] + + # Config file handling + if args.config is not None: + if os.path.exists(args.config): + with open(args.config, 'r') as config: + self.config = yaml.load(config, yaml.CLoader) + else: + raise FileNotFoundError(args.config) + else: + self.config = {} + + # Command line argument handling + self.path = args.path + + def initialize(self): + """ + Finder initialization procedures + + Initializes all Finder subsystems. + + :rtype: bol + :return: True if initialization is successful, False on error + """ + + return True diff --git a/packages/finder/main.py b/packages/finder/main.py new file mode 100644 index 0000000..0f8d79c --- /dev/null +++ b/packages/finder/main.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Finder searcher and browser REPL. +# +# Copyright (C) 2024 Silvio Rhatto <rhatto@riseup.net> +# +# This program 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 any later version. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +# Dependencies +try: + import asyncio +except ImportError: + print("Please install asyncio first!") + raise ImportError + +try: + from prompt_toolkit.application import Application + from prompt_toolkit.filters import Condition, has_focus + from prompt_toolkit.key_binding import KeyBindings + from prompt_toolkit.layout.containers import VSplit, HSplit, Window, ConditionalContainer, WindowAlign + from prompt_toolkit.layout.layout import Layout + from prompt_toolkit.styles import Style + from prompt_toolkit.widgets import SearchToolbar, TextArea + from prompt_toolkit.buffer import Buffer + from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl + from prompt_toolkit.layout.margins import NumberedMargin, ScrollbarMargin + from prompt_toolkit.enums import EditingMode +except ImportError: + print("Please install prompt_toolkit first!") + raise ImportError + +class FinderMain(): + def accept(self, buff): + # Default behavior when pressing Enter in the search field: + # Keep the input query and move to the results buffer + self.app.layout.focus_next() + + return True + + def change(self, _): + self.output_buffer.text = self.format(self.searcher.filter(self.input_field.text)) + + def open(self): + item = self.output_buffer.document.current_line + + self.opener.open(item) + + def bindings(self): + self.keybindings = KeyBindings() + + @self.keybindings.add("c-c") + @self.keybindings.add("c-q") + def _(event): + "Pressing Ctrl-C or Ctrl-Q will exit the user interface." + + event.app.exit() + + @self.keybindings.add("c-d") + def _(event): + "Pressing Ctrl-D with empty input will exit the user interface" + + if self.input_field.text is None or self.input_field.text == '': + event.app.exit() + + @self.keybindings.add("tab") + def _(event): + event.app.layout.focus_next() + + @self.keybindings.add("enter", filter = has_focus(self.output_buffer)) + def _(event): + self.open() + + def theme(self): + self.style = Style.from_dict({ + "output-field" : "#888888", + "input-field" : "bold", + "line" : "#004400", + }) + + def get_statusbar_left(self): + if self.state['loading']: + return "Loading...." + + lines = self.output_buffer.document.line_count + + return f" Results: {lines}" + + def get_statusbar_right(self): + #return "[Tab] change focus [Enter] Select" + return "" + + def ui(self): + # States + self.state = { + 'show_status_bar': True, + 'loading' : True, + } + + # Buffers + self.output_buffer = Buffer() + + # UI fields + self.output_field = BufferControl(buffer=self.output_buffer) + self.input_field = TextArea( + height = 1, + prompt = "finder> ", + style = "class:input-field", + multiline = False, + wrap_lines = False, + ) + + # UI containers + self.container = HSplit( + [ + self.input_field, + Window(height = 1, char = "-", style = "class:line"), + Window( + self.output_field, + cursorline = True, + style = "class:output-field", + left_margins = [NumberedMargin()], + right_margins = [ScrollbarMargin()], + ), + ConditionalContainer( + content=VSplit( + [ + Window( + FormattedTextControl(self.get_statusbar_left), style="class:status" + ), + Window( + FormattedTextControl(self.get_statusbar_right), + style = "class:status.right", + width = 20, + align = WindowAlign.RIGHT, + ), + ], + height = 1, + ), + filter = Condition(lambda: self.state['show_status_bar']), + ), + ] + ) + + async def load(self): + # TODO: logic should be abstracted/generalized + from .plugin.searcher.file.name import FinderPluginFileNameSearcher + from .plugin.opener.file.xdg import FinderPluginFileXdgOpener + + self.searcher = FinderPluginFileNameSearcher(self.path) + self.opener = FinderPluginFileXdgOpener() + self.output_buffer.text = self.format(await self.searcher.finder()) + self.state['loading'] = False + + def run(self): + return asyncio.run(self.main()) + + async def main(self): + # UI + self.ui() + + # Attach handlers to the input field + self.input_field.accept_handler = self.accept + self.input_field.buffer.on_text_changed += self.change + + # The key bindings + self.bindings() + + # Style + self.theme() + + # The application + self.app = Application( + layout = Layout(self.container, focused_element = self.input_field), + key_bindings = self.keybindings, + style = self.style, + full_screen = True, + editing_mode = EditingMode.VI, + ) + + async with asyncio.TaskGroup() as tg: + # Run the application + app = tg.create_task(self.app.run_async()) + + # Load initial buffer content + loader = tg.create_task(self.load()) + + def format(self, items): + return '\n'.join(sorted(items)) + + def finish(self): + # If we have to exit the application depending on the input buffer + # contents + #self.app.exit(result='sometext') + #self.app.exit() + self.app.exit() diff --git a/packages/finder/plugin/opener/file/xdg.py b/packages/finder/plugin/opener/file/xdg.py new file mode 100644 index 0000000..9f2fcca --- /dev/null +++ b/packages/finder/plugin/opener/file/xdg.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Finder searcher and browser REPL. +# +# Copyright (C) 2024 Silvio Rhatto <rhatto@riseup.net> +# +# This program 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 any later version. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import subprocess + +class FinderPluginFileXdgOpener(): + def __init__(self): + pass + + def open(self, file): + process = subprocess.Popen(['xdg-open', file]) diff --git a/packages/finder/plugin/searcher/file/name.py b/packages/finder/plugin/searcher/file/name.py new file mode 100644 index 0000000..2df4963 --- /dev/null +++ b/packages/finder/plugin/searcher/file/name.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Finder searcher and browser REPL. +# +# Copyright (C) 2024 Silvio Rhatto <rhatto@riseup.net> +# +# This program 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 any later version. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +class FinderPluginFileNameSearcher(): + # TODO: check if path exists + def __init__(self, path): + self.setPath(path) + self.items = [] + + def setPath(self, path): + self.path = path + self.items = [] + + async def finder(self, path = ''): + if path == '': + path = self.path + + for parent, dirs, files in os.walk(self.path): + # Ignore Git folders + if '.git' in dirs: + dirs.remove('.git') + continue + + # Currently we're not listing folders + #for folder in dirs: + # self.items.append(os.path.join(parent, folder)) + + for file in files: + self.items.append(os.path.join(parent, file)) + + return self.items + + def filter(self, text = ''): + if text == '' or text is None: + return self.items + + return filter(lambda s: re.match(".*" + re.escape(text) + ".*", s), self.items) |