diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..fa093fe --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +buy_me_a_coffee: aitjcize diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6fd122d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +name: Release and Publish to PyPI + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Wait for tests to pass + uses: lewagon/wait-on-check-action@v1.3.4 + with: + ref: ${{ github.ref }} + check-name: 'test' + repo-token: ${{ secrets.GITHUB_TOKEN }} + wait-interval: 10 + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true + files: | + dist/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + twine upload dist/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index baeeb9c..704b96e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: Python CI on: [push, pull_request] jobs: - build: + test: runs-on: ubuntu-24.04 strategy: matrix: diff --git a/AUTHORS b/AUTHORS index c11758e..d5c8b48 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,6 +5,7 @@ Wei-Ning Huang Contributors ------------ Alexander 'z33ky' Hirsch <1zeeky@gmail.com> +belkka Brian Foley ChangZhuo Chen (陳昌倬) Chris Smith @@ -20,6 +21,4 @@ Matan Rosenberg Reverend Homer Rui Chen Simon Gene Gottlieb -belkka -glenvt18 -taiyu +taiyu \ No newline at end of file diff --git a/ChangeLog b/ChangeLog index f6f95f4..8e6f633 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,24 @@ cppman Changelog ------------------------------------------------------------------------------ +cppman 0.6.0 (Dec 19th, 2025): + + Features added: + * Implement hybrid search strategy with smart std:: prefix matching + * Show first match directly by default (no selection menu) + * Move interactive selection menu to -f flag + * Add pagination support to selection menu (20 items per page) + * Navigation commands: 'n'/'next' and 'p'/'prev' for page navigation + * Refactor version management to use single source of truth (__version__.py) + * Add GitHub Actions workflow for automated releases to PyPI + * Update documentation (help text and man page) + +cppman 0.5.9 (Apr 10th, 2025): + + Features added: + * Present users with a menu of keyword results in multiple hits + * update index.db + cppman 0.5.7 (Jun 27th, 2024): Bug fixed: diff --git a/MANIFEST.in b/MANIFEST.in index ad31be7..3913a60 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ include bin/* include cppman/* include cppman/formatter/* include misc/* +include requirements.txt include README.rst include AUTHORS include COPYING diff --git a/README.rst b/README.rst index add7034..245ccf8 100644 --- a/README.rst +++ b/README.rst @@ -5,9 +5,9 @@ cppman ====== -C++ 98/11/14/17/20 manual pages for Linux, with source from `cplusplus.com `_ and `cppreference.com `_. +C++ manual pages for Linux, with source from `cplusplus.com `_ and `cppreference.com `_, supporting all C++ versions provided by the sources. -.. image:: https://raw.github.com/aitjcize/cppman/master/wiki/screenshot.png +.. image:: https://raw.github.com/aitjcize/cppman/main/wiki/screenshot.png Features -------- @@ -29,7 +29,7 @@ Demo ---- Using vim as pager -.. image:: https://raw.github.com/aitjcize/cppman/master/wiki/demo.gif +.. image:: https://raw.github.com/aitjcize/cppman/main/wiki/demo.gif Installation ------------ @@ -82,7 +82,7 @@ FAQ * Q: Can I use the system ``man`` command instead of ``cppman``? * A: Yes, just execute ``cppman -m true`` and all cached man pages are exposed to the system ``man`` command. Note: You may want to download all available man pages with ``cppman -c``. * Q: Why is bash completion is not working properly with ``::``? -* A: It is because bash treats ``:`` like a white space. To fix this add ``export COMP_WORDBREAKS=" /\"\'><;|&("`` to your ``~/.bashrc``. +* A: It is because bash treats ``:`` like a white space. To fix this add ``export COMP_WORDBREAKS="${COMP_WORDBREAKS//:}"`` to your ``~/.bashrc``. Bugs ---- @@ -96,6 +96,15 @@ Contributing 4. Push to the branch (``git push origin my-new-feature``) 5. Create new Pull Request +Support +------- + +If you find this project useful, consider buying me a coffee! ☕ + +.. image:: https://img.shields.io/badge/Buy%20Me%20A%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black + :target: https://buymeacoffee.com/aitjcize + :alt: Buy Me A Coffee + Notes ----- * manpages-cpp is renamed to cppman since September 19, 2012 diff --git a/bin/cppman b/bin/cppman index a579c85..ba2ef86 100755 --- a/bin/cppman +++ b/bin/cppman @@ -40,9 +40,10 @@ if program.startswith('./') or program.startswith('bin/'): from cppman.main import Cppman from cppman.environ import config from cppman.util import update_mandb_path, update_man3_link +from cppman.__version__ import __version__ program_name = sys.argv[0] -program_version = '0.5.7' +program_version = __version__ def version(): @@ -70,7 +71,7 @@ def main(): help='Clear all cached files.'), make_option('-f', '--find-page', action='store', type='string', dest='keyword', default=None, - help='Find man page.'), + help='Find man page and show selection menu if multiple matches.'), make_option('-o', '--force-update', action='store_true', dest='force', default=False, help="Force cppman to update existing cache when " @@ -91,11 +92,16 @@ def main(): make_option('-v', '--version', action='store_true', dest='version', default=False, help='Show version information.'), make_option('--force-columns', action='store', dest='force_columns', - type=int, default=-1, help='Force terminal columns.') + type=int, default=-1, help='Force terminal columns.'), + make_option('-n','--max-results', action='store', dest='max_results', + type=int, default=-1, help='Maximum number of search results to show.') ] parser = OptionParser( - usage='Usage: cppman [OPTION...] PAGE...', option_list=option_list) + usage='Usage: cppman [OPTION...] PAGE...\n\n' + 'When PAGE is specified without -f, cppman shows the first match directly.\n' + 'Search automatically tries std:: prefix first for better results.', + option_list=option_list) options, args = parser.parse_args() @@ -108,15 +114,23 @@ def main(): cm.cache_all() sys.exit(0) + cm = Cppman() + if options.clear_cache: - cm = Cppman() cm.clear_cache() sys.exit(0) if options.keyword: - cm = Cppman() - cm.find(options.keyword) - sys.exit(0) + try: + keyword = cm.fuzzy_find(options.keyword, options.max_results, show_menu=True) + if not keyword: + sys.exit(1) + pid = cm.man(keyword) + os.waitpid(pid, 0) + sys.exit(0) + except RuntimeError as e: + print(e, file=sys.stderr) + sys.exit(16) if options.source: if options.source not in config.SOURCES: @@ -147,7 +161,6 @@ def main(): sys.exit(0) if options.rebuild_index: - cm = Cppman() cm.rebuild_index() sys.exit(0) @@ -155,11 +168,12 @@ def main(): sys.stderr.write('What manual page do you want?\n') sys.exit(1) - cm = Cppman(options.force, options.force_columns) - failure = False - try: - pid = cm.man(args[0]) + keyword = cm.fuzzy_find(args[0], options.max_results, show_menu=False) + if not keyword: + sys.exit(1) + + pid = cm.man(keyword) except RuntimeError as e: print(e, file=sys.stderr) sys.exit(16) diff --git a/cppman/__version__.py b/cppman/__version__.py new file mode 100644 index 0000000..ef7eb44 --- /dev/null +++ b/cppman/__version__.py @@ -0,0 +1 @@ +__version__ = '0.6.0' diff --git a/cppman/config.py b/cppman/config.py index 1e6a980..fa88a85 100644 --- a/cppman/config.py +++ b/cppman/config.py @@ -33,7 +33,7 @@ class Config(object): DEFAULTS = { 'Source': 'cppreference.com', 'UpdateManPath': 'false', - 'Pager': 'vim' + 'Pager': 'less' } def __init__(self, configfile): diff --git a/cppman/lib/index.db b/cppman/lib/index.db index 24ff923..1c818e5 100644 Binary files a/cppman/lib/index.db and b/cppman/lib/index.db differ diff --git a/cppman/main.py b/cppman/main.py index 3a6576e..8f8b0d9 100644 --- a/cppman/main.py +++ b/cppman/main.py @@ -537,6 +537,21 @@ def _search_keyword(self, pattern): self.source = environ.source self.cursor.execute('PRAGMA case_sensitive_like=ON') + + std_results = [] + if not pattern.startswith('std::'): + std_pattern = 'std::' + pattern + std_results = self._fetch_page_by_keyword("%s" % std_pattern) + std_results.extend(self._fetch_page_by_keyword("%s %%" % std_pattern)) + std_results.extend(self._fetch_page_by_keyword("%% %s" % std_pattern)) + std_results.extend(self._fetch_page_by_keyword("%% %s %%" % std_pattern)) + std_results.extend(self._fetch_page_by_keyword("%s%%" % std_pattern)) + std_results = list(set(std_results)) + + if len(std_results) >= 5: + conn.close() + return sorted(std_results, key=lambda e: _sort_search(e, std_pattern)) + results = self._fetch_page_by_keyword("%s" % pattern) results.extend(self._fetch_page_by_keyword("%s %%" % pattern)) results.extend(self._fetch_page_by_keyword("%% %s" % pattern)) @@ -546,6 +561,8 @@ def _search_keyword(self, pattern): if len(results) == 0: results = self._fetch_page_by_keyword("%%%s%%" % pattern) + results.extend(std_results) + conn.close() return sorted(list(set(results)), key=lambda e: _sort_search(e, pattern)) @@ -596,6 +613,64 @@ def find(self, pattern): else: raise RuntimeError('%s: nothing appropriate.' % pattern) + def fuzzy_find(self, pattern, max_results, show_menu=False): + """Find pages in database and optionally present an interactive selection menu.""" + results = self._search_keyword(pattern) + if max_results >= 1: + results = results[:max_results] + + if not results: + raise RuntimeError('%s: nothing appropriate.' % pattern) + + if len(results) == 1: + return results[0][1] + + if not show_menu: + return results[0][1] + + page_size = 20 + current_page = 0 + total_pages = (len(results) + page_size - 1) // page_size + + while True: + start_idx = current_page * page_size + end_idx = min(start_idx + page_size, len(results)) + + print(f"\n--- Page {current_page + 1}/{total_pages} ---") + for i in range(start_idx, end_idx): + name, keyword, url = results[i] + print(f"{i + 1}. {keyword} - {name}") + + if total_pages > 1: + nav_help = [] + if current_page > 0: + nav_help.append("'p' for previous") + if current_page < total_pages - 1: + nav_help.append("'n' for next") + print(f"\nEnter number to select, {', '.join(nav_help)}, or press Enter to cancel") + + try: + selection = input("\nPlease enter the selection: ").strip().lower() + if not selection: + return None + + if selection in ('n', 'next') and current_page < total_pages - 1: + current_page += 1 + continue + elif selection in ('p', 'prev', 'previous') and current_page > 0: + current_page -= 1 + continue + + idx = int(selection) - 1 + if 0 <= idx < len(results): + return results[idx][1] + print("Invalid selection. Please try again.") + except ValueError: + print("Please enter a valid number or navigation command.") + except KeyboardInterrupt: + print("\nOperation cancelled.") + return None + def update_mandb(self, quiet=True): """Update mandb.""" if not environ.config.UpdateManPath: diff --git a/dev/chver.sh b/dev/chver.sh deleted file mode 100755 index da25c85..0000000 --- a/dev/chver.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -sed -i "s/program_version = '.*'/program_version = '$1'/" bin/cppman -sed -i "s/version = '.*'/version = '$1'/" setup.py diff --git a/dev/update_authors.sh b/dev/update_authors.sh index 5cd9279..fcf7d1f 100755 --- a/dev/update_authors.sh +++ b/dev/update_authors.sh @@ -1,6 +1,6 @@ #!/bin/bash -AUTHOR='AZ Huang ' +AUTHOR='Wei-Ning Huang ' cat << EOF > AUTHORS Developers diff --git a/misc/cppman.1 b/misc/cppman.1 index 54dceba..14e2ebe 100644 --- a/misc/cppman.1 +++ b/misc/cppman.1 @@ -6,9 +6,11 @@ cppman - C++ manual page viewer / fetcher .I OPTIONS... .B ] PAGE... .SH DESCRIPTION -cppman generates C++ manual pages from cplusplus.com and provide a man\-like interface to view man pages. +cppman generates C++ manual pages from cplusplus.com and cppreference.com and provides a man\-like interface to view man pages. .sp By default, cppman fetches man pages on-the-fly, by running the command 'cppman \-c', all available manpages are cached, making offline browsing possible. This is also required if you want to use the system 'man' command. +.sp +When searching for a page (e.g., 'cppman map'), cppman automatically tries the std:: prefix first (std::map) for better results. If a page name is specified without the \-f option, cppman displays the first match directly. Use \-f to show an interactive selection menu with pagination support (20 items per page). .SS Browsing man pages cppman uses Vi Improved as a pager. .br @@ -24,7 +26,7 @@ cache all available man pages from cplusplus.com to enable offline browsing .IP "\-C, \-\-clear\-cache" clear all cached files .IP "\-f KEYWORD, \-\-find\-page=KEYWORD" -find man page +find man page and show interactive selection menu if multiple matches are found. The menu displays up to 20 results per page. Use 'n' or 'next' to go to the next page, 'p' or 'prev' to go to the previous page, or enter a number to select a specific page. .IP "\-o, \-\-force\-update" force cppman to update existing cache when '\-\-cache\-all' or browsing man pages that were already cached .IP "\-m MANDB, \-\-use\-mandb=MANDB" @@ -33,7 +35,9 @@ Accepts 'true' or 'false'. If true, cppman adds manpage path to mandb so that yo Select pager to use, accepts 'vim', 'nvim' or 'less'. The default value is 'vim'. If 'nvim' is selected, but not available, 'vim' is used as a fallback and vice versa. If either is selected, but neither is available, 'less' is used as a fallback. .IP "\-r, \-\-rebuild\-index" -rebuild index database from cplusplus.com +rebuild index database from cplusplus.com and cppreference.com +.IP "\-n NUM, \-\-max\-results=NUM" +maximum number of search results to show in the selection menu .IP "\-v, \-\-version" show version information .IP "\-h, \-\-help" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4dd7165 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +beautifulsoup4==4.13.3 +bs4==0.0.2 +html5lib==1.1 +lxml==5.3.2 +six==1.17.0 +soupsieve==2.6 +typing_extensions==4.13.1 +webencodings==0.5.1 diff --git a/setup.py b/setup.py index 4e8f832..a8921ad 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,17 @@ #!/usr/bin/env python from distutils.core import setup +import os + +def get_version(): + version_file = os.path.join(os.path.dirname(__file__), 'cppman', '__version__.py') + with open(version_file, 'r') as f: + for line in f: + if line.startswith('__version__'): + return line.split('=')[1].strip().strip("'\"") + raise RuntimeError('Unable to find version string.') + +__version__ = get_version() _package_data = [ 'lib/index.db', @@ -16,10 +27,18 @@ ('share/fish/vendor_completions.d/', ['misc/completions/fish/cppman.fish']) ] +with open('requirements.txt') as f: + _requirements = f.read().splitlines() + +with open('README.rst', encoding='utf-8') as f: + _long_description = f.read() + setup( name = 'cppman', - version = '0.5.7', - description = 'C++ 98/11/14/17/20 manual pages for Linux/MacOS', + version = __version__, + description = 'C++ manual pages for Linux/MacOS', + long_description = _long_description, + long_description_content_type = 'text/x-rst', author = 'Wei-Ning Huang (AZ)', author_email = 'aitjcize@gmail.com', url = 'https://github.com/aitjcize/cppman', @@ -28,7 +47,7 @@ package_data = {'cppman': _package_data}, data_files = _data_files, scripts = ['bin/cppman'], - install_requires=['beautifulsoup4', 'html5lib'], + install_requires=_requirements, classifiers = [ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6',