Skip to content
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
1 change: 1 addition & 0 deletions devchat/chatmark/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tmp/
5 changes: 5 additions & 0 deletions devchat/chatmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# ChatMark

ChatMark is a markup language for user interaction in chat message.

This module provides python implementation for common widgets in ChatMark.
12 changes: 12 additions & 0 deletions devchat/chatmark/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from .form import Form
from .step import Step
from .widgets import Button, Checkbox, Radio, TextEditor

__all__ = [
"Checkbox",
"TextEditor",
"Radio",
"Button",
"Form",
"Step",
]
16 changes: 16 additions & 0 deletions devchat/chatmark/chatmark_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# chatmark_exmaple

This is an example of how to use the chatmark module.

Usage:

1. Copy the `chatmark_example` folder under `~/.chat/workflow/org`
2. Create `command.yml` under `~/.chat/workflow/org/chatmark_example` with the following content:
```yaml
description: chatmark examples
steps:
- run: $command_python $command_path/main.py

```
3. Use the command `/chatmark_example` in devchat vscode plugin.

159 changes: 159 additions & 0 deletions devchat/chatmark/chatmark_example/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import time


from devchat.chatmark import Button, Checkbox, Form, Radio, Step, TextEditor # pylint: disable=E402


def main():
print("\n\n---\n\n")

# Step
print("\n\n# Step Example\n\n")
with Step("Something is running..."):
print("Will sleep for 5 seconds...", flush=True)
time.sleep(5)
print("Done", flush=True)

print("\n\n# Step Example with exception\n\n")
try:
with Step("Something is running (will raise exception)..."):
print("Will sleep for 5 seconds...", flush=True)
time.sleep(5)
raise Exception("oops!")

except Exception:
pass

# Button
print("\n\n# Button Example\n\n")
button = Button(
[
"Yes",
"Or",
"No",
],
)
button.render()

idx = button.clicked
print("\n\nButton result\n\n")
print(f"\n\n{idx}: {button.buttons[idx]}\n\n")

print("\n\n---\n\n")

# Checkbox
print("\n\n# Checkbox Example\n\n")
checkbox = Checkbox(
[
"A",
"B",
"C",
"D",
],
[True, False, False, True],
)
checkbox.render()

print(f"\n\ncheckbox.selections: {checkbox.selections}\n\n")
for idx in checkbox.selections:
print(f"\n\n{idx}: {checkbox.options[idx]}\n\n")

print("\n\n---\n\n")

# TextEditor
print("\n\n# TextEditor Example\n\n")
text_editor = TextEditor(
"hello world\nnice to meet you",
)

text_editor.render()

print(f"\n\ntext_editor.new_text:\n\n{text_editor.new_text}\n\n")

print("\n\n---\n\n")

# Radio
print("\n\n# Radio Example\n\n")
radio = Radio(
[
"Sun",
"Moon",
"Star",
],
)
radio.render()

print(f"\n\nradio.selection: {radio.selection}\n\n")
if radio.selection is not None:
print(f"\n\nradio.options[radio.selection]: {radio.options[radio.selection]}\n\n")

print("\n\n---\n\n")

# Form
print("\n\n# Form Example\n\n")
checkbox_1 = Checkbox(
[
"Sprint",
"Summer",
"Autumn",
"Winter",
]
)
checkbox_2 = Checkbox(
[
"金",
"木",
"水",
"火",
"土",
],
)
radio_1 = Radio(
[
"Up",
"Down",
],
)
radio_2 = Radio(
[
"Left",
"Center",
"Right",
],
)
text_editor_1 = TextEditor(
"hello world\nnice to meet you",
)
text_editor_2 = TextEditor(
"hihihihihi",
)

form = Form(
[
"Some string in a form",
checkbox_1,
"Another string in a form",
radio_1,
"the third string in a form",
checkbox_2,
"the fourth string in a form",
radio_2,
"the fifth string in a form",
text_editor_1,
"the last string in a form",
text_editor_2,
],
)

form.render()

print(f"\n\ncheckbox_1.selections: {checkbox_1.selections}\n\n")
print(f"\n\ncheckbox_2.selections: {checkbox_2.selections}\n\n")
print(f"\n\nradio_1.selection: {radio_1.selection}\n\n")
print(f"\n\nradio_2.selection: {radio_2.selection}\n\n")
print(f"\n\ntext_editor_1.new_text:\n\n{text_editor_1.new_text}\n\n")
print(f"\n\ntext_editor_2.new_text:\n\n{text_editor_2.new_text}\n\n")


if __name__ == "__main__":
main()
97 changes: 97 additions & 0 deletions devchat/chatmark/form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# pylint: disable=C0103
# pylint: disable=W0212
from typing import Dict, List, Optional, Union

from .iobase import pipe_interaction
from .widgets import Button, Widget


class Form:
"""
A container for different widgets

Syntax:
"""

def __init__(
self,
components: List[Union[Widget, str]],
title: Optional[str] = None,
submit_button_name: Optional[str] = None,
cancel_button_name: Optional[str] = None,
):
"""
components: components in the form, can be widgets (except Button) or strings
title: title of the form
"""
assert (
any(isinstance(c, Button) for c in components) is False
), "Button is not allowed in Form"

self._components = components
self._title = title

self._rendered = False
self._submit = submit_button_name
self._cancel = cancel_button_name

@property
def components(self) -> List[Union[Widget, str]]:
"""
Return the components
"""

return self._components

def _in_chatmark(self) -> str:
"""
Generate ChatMark syntax for all components
"""
lines = []

if self._title:
lines.append(self._title)

for c in self.components:
if isinstance(c, str):
lines.append(c)
elif isinstance(c, Widget):
lines.append(c._in_chatmark())
else:
raise ValueError(f"Invalid component {c}")

return "\n".join(lines)

def _parse_response(self, response: Dict):
"""
Parse response from user input
"""
for c in self.components:
if isinstance(c, Widget):
c._parse_response(response)

def render(self):
"""
Render to receive user input
"""
if self._rendered:
# already rendered once
# not sure if the constraint is necessary
# could be removed if re-rendering is needed
raise RuntimeError("Widget can only be rendered once")

self._rendered = True

chatmark_header = "```chatmark"
chatmark_header += f" submit={self._submit}" if self._submit else ""
chatmark_header += f" cancel={self._cancel}" if self._cancel else ""

lines = [
chatmark_header,
self._in_chatmark(),
"```",
]

chatmark = "\n".join(lines)
response = pipe_interaction(chatmark)
self._parse_response(response)
43 changes: 43 additions & 0 deletions devchat/chatmark/iobase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import yaml


def _send_message(message):
out_data = f"""\n{message}\n"""
print(out_data, flush=True)


def _parse_chatmark_response(response):
# resonse looks like:
"""
``` some_name
some key name 1: value1
some key name 2: value2
```
"""
# parse key values
lines = response.strip().split("\n")
if len(lines) <= 2:
return {}

data = yaml.safe_load("\n".join(lines[1:-1]))
return data


def pipe_interaction(message: str):
_send_message(message)

lines = []
while True:
try:
line = input()
if line.strip().startswith("```yaml"):
lines = []
elif line.strip() == "```":
lines.append(line)
break
lines.append(line)
except EOFError:
pass

response = "\n".join(lines)
return _parse_chatmark_response(response)
28 changes: 28 additions & 0 deletions devchat/chatmark/step.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from contextlib import AbstractContextManager


class Step(AbstractContextManager):
"""
Show a running step in the TUI.

ChatMark syntax:

```Step
# Something is running...
some details...
```

Usage:
with Step("Something is running..."):
print("some details...")
"""

def __init__(self, title: str):
self.title = title

def __enter__(self):
print(f"\n```Step\n# {self.title}", flush=True)

def __exit__(self, exc_type, exc_val, exc_tb):
# close the step
print("\n```", flush=True)
Loading