Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command line tweaking #24

Merged
merged 15 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@
([#15](https://github.com/davep/hike/pull/15))
- Made `chdir` a lot less fussy about the path given.
([#18](https://github.com/davep/hike/pull/18))
- Added `quit` as a command that the command line understands.
([#24](https://github.com/davep/hike/pull/24))
- Added `dir` and `ls` as aliases for `chdir`.
([#24](https://github.com/davep/hike/pull/24))
- Added `contents` as a command that the command line understands.
([#24](https://github.com/davep/hike/pull/24))
- Added `bookmarks` as a command that the command line understands.
([#24](https://github.com/davep/hike/pull/24))
- Added `history` as a command that the command line understands.
([#24](https://github.com/davep/hike/pull/24))
- Added `local` as a command that the command line understands.
([#24](https://github.com/davep/hike/pull/24))

## v0.2.0

Expand Down
32 changes: 32 additions & 0 deletions src/hike/widgets/command_line/base_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,37 @@ def handle(cls, text: str, for_widget: Widget) -> bool:
"""
return False

@staticmethod
def split_command(text: str) -> tuple[str, str]:
"""Split the command for further testing.

Args:
text: The text of the command.

Returns:
The command and its tail.
"""
command, _, tail = text.strip().partition(" ")
return command.strip(), tail.strip()

@classmethod
def is_command(cls, command: str) -> bool:
"""Does the given command appear to be a match?

Args:
command: The command to test.

Returns:
`True` if the given command seems to be a match, `False` if not.
"""
# Build up all the possible matches. These are built from the main
# command and also the aliases. By convention the code will often
# use `code` fences for commands, and the aliases will be a comma
# list, so we clean that up as we go...
return command.strip().lower() in (
candidate.strip().lower().removeprefix("`").removesuffix("`")
for candidate in (cls.COMMAND, *cls.ALIASES.split(","))
)


### base_command.py ends here
23 changes: 9 additions & 14 deletions src/hike/widgets/command_line/change_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
##############################################################################
# Python imports.
from pathlib import Path
from re import Pattern, compile
from typing import Final

##############################################################################
# Textual imports.
Expand All @@ -15,17 +13,13 @@
from ...messages import SetLocalViewRoot
from .base_command import InputCommand

##############################################################################
CHDIR: Final[Pattern[str]] = compile(r"^\s*(chdir|cd)\s+(?P<directory>.+)$")
"""Regular expression for matching the command."""


##############################################################################
class ChangeDirectoryCommand(InputCommand):
"""Change the root directory of the local file browser"""

COMMAND = "`chdir`"
ALIASES = "`cd`"
ALIASES = "`cd`, `dir`, `ls`"
ARGUMENTS = "`<directory>`"

@classmethod
Expand All @@ -39,13 +33,14 @@ def handle(cls, text: str, for_widget: Widget) -> bool:
Returns:
`True` if the command was handled; `False` if not.
"""
if match := CHDIR.search(text):
if (
match["directory"]
and (root := Path(match["directory"].strip()).expanduser()).is_dir()
):
for_widget.post_message(SetLocalViewRoot(Path(root).resolve()))
return True
command, directory = cls.split_command(text)
if (
cls.is_command(command)
and directory
and (root := Path(directory).expanduser()).is_dir()
):
for_widget.post_message(SetLocalViewRoot(Path(root).resolve()))
return True
return False


Expand Down
93 changes: 93 additions & 0 deletions src/hike/widgets/command_line/general.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Provides general application commands for the command line."""

##############################################################################
# Textual imports.
from textual.message import Message
from textual.widget import Widget

##############################################################################
# Textual enhanced imports.
from textual_enhanced.commands import Quit

##############################################################################
# Local imports.
from ...commands import (
JumpToBookmarks,
JumpToHistory,
JumpToLocalBrowser,
JumpToTableOfContents,
)
from .base_command import InputCommand


##############################################################################
class GeneralCommand(InputCommand):
"""Base class for general commands."""

COMMAND = "`quit`"
ALIASES = "`q`"
MESSAGE: type[Message]

@classmethod
def handle(cls, text: str, for_widget: Widget) -> bool:
"""Handle the command.

Args:
text: The text of the command.
for_widget: The widget to handle the command for.

Returns:
`True` if the command was handled; `False` if not.
"""
if cls.is_command(text):
for_widget.post_message(cls.MESSAGE())
return True
return False


##############################################################################
class BookmarksCommand(GeneralCommand):
"""Jump to the bookmarks"""

COMMAND = "`bookmarks`"
ALIASES = "`b`, `bm`"
MESSAGE = JumpToBookmarks


##############################################################################
class ContentsCommand(GeneralCommand):
"""Jump to the table of contents"""

COMMAND = "`contents`"
ALIASES = "`c`, `toc`"
MESSAGE = JumpToTableOfContents


##############################################################################
class HistoryCommand(GeneralCommand):
"""Jump to the browsing history"""

COMMAND = "`history`"
ALIASES = "`h`"
MESSAGE = JumpToHistory


##############################################################################
class LocalCommand(GeneralCommand):
"""Jump to the local file browser"""

COMMAND = "`local`"
ALIASES = "`l`"
MESSAGE = JumpToLocalBrowser


##############################################################################
class QuitCommand(GeneralCommand):
"""Quit the application"""

COMMAND = "`quit`"
ALIASES = "`q`"
MESSAGE = Quit


### general.py ends here
27 changes: 6 additions & 21 deletions src/hike/widgets/command_line/open_from_forge.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

##############################################################################
# Local imports.
from ...data import load_configuration
from ...messages import OpenFromForge
from .base_command import InputCommand

Expand All @@ -19,7 +20,7 @@
class OpenFromForgeCommand(InputCommand):
"""Base class for commands that open a file from a forge."""

ARGUMENTS = "`<owner> <repo>[:<branch>] [<file>]`"
ARGUMENTS = "`<remote-file>"

WITHOUT_BRANCH: Final[Pattern[str]] = compile(
r"^(?P<owner>[^/ ]+)[/ ](?P<repo>[^ :]+)(?: +(?P<file>[^ ]+))?$"
Expand All @@ -37,7 +38,7 @@ class OpenFromForgeCommand(InputCommand):
URL_FORMAT = ""
"""The format of the raw URL for the forge."""

HELP = """
HELP = f"""
| Format | Effect |
| -- | -- |
| `<owner>/<repo>` | Open `README.md` from a repository |
Expand All @@ -49,24 +50,10 @@ class OpenFromForgeCommand(InputCommand):
| `<owner>/<repo>:<branch> <file>` | Open a specific file from a specific branch of a repository |
| `<owner> <repo>:<branch> <file>` | Open a specific file from a specific branch of a repository |

If `<branch>` is omitted the requested file is looked for first in the
`main` branch and then `master`.
If `<branch>` is omitted the requested file is looked in the following branches:
{', '.join(f'`{branch}`' for branch in load_configuration().main_branches)}.
"""

@staticmethod
def split_command(text: str) -> tuple[str, str]:
"""Split the command for further testing.

Args:
text: The text of the command.

Returns:
The command and its arguments.
"""
if len(candidate := text.split(maxsplit=1)) == 1:
return candidate[0], ""
return (candidate[0], candidate[1]) if candidate else ("", "")

@classmethod
def maybe_request(cls, arguments: str, for_widget: Widget) -> bool:
"""Maybe request a file be opened from the given forge.
Expand Down Expand Up @@ -115,9 +102,7 @@ def handle(cls, text: str, for_widget: Widget) -> bool:
`True` if the command was handled; `False` if not.
"""
command, arguments = cls.split_command(text)
return f"`{command}`" in (cls.COMMAND, cls.ALIASES) and cls.maybe_request(
arguments, for_widget
)
return cls.is_command(command) and cls.maybe_request(arguments, for_widget)


##############################################################################
Expand Down
3 changes: 1 addition & 2 deletions src/hike/widgets/command_line/open_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
# Textual imports.
from textual.widget import Widget

from ...data import looks_urllike

##############################################################################
# Local imports.
from ...data import looks_urllike
from ...messages import OpenLocation
from .base_command import InputCommand

Expand Down
16 changes: 14 additions & 2 deletions src/hike/widgets/command_line/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
from ...types import CommandHistory
from .base_command import InputCommand
from .change_directory import ChangeDirectoryCommand
from .general import (
BookmarksCommand,
ContentsCommand,
HistoryCommand,
LocalCommand,
QuitCommand,
)
from .open_directory import OpenDirectoryCommand
from .open_file import OpenFileCommand
from .open_from_forge import (
Expand All @@ -48,11 +55,16 @@
OpenDirectoryCommand,
OpenURLCommand,
# Once the above are out of the way the order doesn't matter so much.
BookmarksCommand,
ChangeDirectoryCommand,
HistoryCommand,
LocalCommand,
OpenFromBitbucket,
OpenFromCodeberg,
OpenFromGitHub,
OpenFromGitLab,
ContentsCommand,
QuitCommand,
)
"""The commands used for the input."""

Expand Down Expand Up @@ -111,9 +123,9 @@ class CommandLine(Vertical):

| Command | Aliases | Arguments | Description |
| -- | -- | -- | -- |
{'\n '.join(command.help_text() for command in COMMANDS)}
{'\n '.join(sorted(command.help_text() for command in COMMANDS))}

### Forge support
### ¹Forge support

The forge-oriented commands listed above accept a number of different
ways of quickly specifying which file you want to view. Examples include:
Expand Down
60 changes: 60 additions & 0 deletions tests/unit/test_command_line_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Tests for the command line command matching code."""

##############################################################################
# Pytest imports.
from pytest import mark

##############################################################################
# Local imports.
from hike.widgets.command_line.base_command import InputCommand


##############################################################################
class ExampleCommand(InputCommand):
COMMAND = "`test`"
ALIASES = "`t`, `tester`, `testing`"


##############################################################################
@mark.parametrize(
"look_for, found",
(
("test", True),
("t", True),
("tester", True),
("testing", True),
("TEST", True),
("T", True),
("TESTER", True),
("TESTING", True),
(" TeST ", True),
(" T ", True),
(" TEstER ", True),
(" TESting ", True),
("", False),
("te", False),
),
)
def test_is_command(look_for: str, found: bool) -> None:
"""We should be able to find if a command is a match."""
assert ExampleCommand.is_command(look_for) is found


##############################################################################
@mark.parametrize(
"split, result",
(
("", ("", "")),
("test", ("test", "")),
(" test ", ("test", "")),
("test test", ("test", "test")),
(" test test ", ("test", "test")),
("test test test test", ("test", "test test test")),
),
)
def test_split_command(split: str, result: tuple[str, str]) -> None:
"""The command splitting code should work as expected."""
assert ExampleCommand.split_command(split) == result


### test_command_line_commands.py ends here
Loading