aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/finder/__init__.py0
-rw-r--r--packages/finder/app.py128
-rw-r--r--packages/finder/config.py137
-rw-r--r--packages/finder/init.py77
-rw-r--r--packages/finder/main.py207
-rw-r--r--packages/finder/plugin/opener/file/xdg.py28
-rw-r--r--packages/finder/plugin/searcher/file/name.py57
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)