autosh 0.0.2__py3-none-any.whl → 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autosh/config-template.toml +13 -0
- autosh/config.py +5 -0
- autosh/main.py +7 -8
- autosh/md/__init__.py +8 -0
- autosh/md/inline_text.py +214 -0
- autosh/md/printer.py +301 -0
- autosh/md/state.py +136 -0
- autosh/md/stream.py +107 -0
- autosh/plugins/__init__.py +2 -2
- autosh/session.py +16 -2
- {autosh-0.0.2.dist-info → autosh-0.0.3.dist-info}/METADATA +8 -4
- autosh-0.0.3.dist-info/RECORD +22 -0
- autosh/md.py +0 -408
- autosh-0.0.2.dist-info/RECORD +0 -17
- {autosh-0.0.2.dist-info → autosh-0.0.3.dist-info}/WHEEL +0 -0
- {autosh-0.0.2.dist-info → autosh-0.0.3.dist-info}/entry_points.txt +0 -0
- {autosh-0.0.2.dist-info → autosh-0.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
# autosh configuration file
|
2
|
+
[autosh]
|
3
|
+
api_key = "sk-or-v1-..."
|
4
|
+
# model = "openai/gpt-4.1"
|
5
|
+
# think_model = "openai/o4-mini-high"
|
6
|
+
|
7
|
+
[plugins]
|
8
|
+
calc = {}
|
9
|
+
cli = {}
|
10
|
+
clock = {}
|
11
|
+
code = {}
|
12
|
+
# search = { tavily_api_key = "tvly-dev-..." }
|
13
|
+
# web = { tavily_api_key = "tvly-dev-..." }
|
autosh/config.py
CHANGED
@@ -43,6 +43,11 @@ class Config(BaseModel):
|
|
43
43
|
|
44
44
|
@staticmethod
|
45
45
|
def load() -> "Config":
|
46
|
+
if not USER_CONFIG_PATH.is_file():
|
47
|
+
# Copy config.template.toml to USER_CONFIG_PATH
|
48
|
+
USER_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
49
|
+
template = Path(__file__).parent / "config.template.toml"
|
50
|
+
USER_CONFIG_PATH.write_text(template.read_text())
|
46
51
|
if USER_CONFIG_PATH.is_file():
|
47
52
|
try:
|
48
53
|
doc = tomllib.loads(USER_CONFIG_PATH.read_text())
|
autosh/main.py
CHANGED
@@ -8,7 +8,7 @@ from rich.columns import Columns
|
|
8
8
|
from rich.panel import Panel
|
9
9
|
import argparse
|
10
10
|
|
11
|
-
from autosh.config import CLI_OPTIONS, CONFIG
|
11
|
+
from autosh.config import CLI_OPTIONS, CONFIG, USER_CONFIG_PATH
|
12
12
|
from .session import Session
|
13
13
|
import sys
|
14
14
|
|
@@ -149,14 +149,13 @@ def main():
|
|
149
149
|
# dotenv.load_dotenv()
|
150
150
|
prompt, args = parse_args()
|
151
151
|
|
152
|
+
if key := os.getenv("OPENROUTER_API_KEY"):
|
153
|
+
CONFIG.api_key = key
|
152
154
|
if CONFIG.api_key is None:
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
"[bold red]Error:[/bold red] [red]No API key found. Please set the OPENROUTER_API_KEY environment variable or add it to your config file.[/red]"
|
158
|
-
)
|
159
|
-
sys.exit(1)
|
155
|
+
rich.print(
|
156
|
+
f"[bold red]Error:[/bold red] [red]OpenRouter API key not found.\nPlease set the OPENROUTER_API_KEY environment variable or add it to your config file: {USER_CONFIG_PATH}.[/red]"
|
157
|
+
)
|
158
|
+
sys.exit(1)
|
160
159
|
try:
|
161
160
|
asyncio.run(start_session(prompt, args))
|
162
161
|
except (KeyboardInterrupt, EOFError):
|
autosh/md/__init__.py
ADDED
autosh/md/inline_text.py
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from autosh.md.printer import StreamedMarkdownPrinter
|
3
|
+
from autosh.md.state import State
|
4
|
+
|
5
|
+
|
6
|
+
@dataclass
|
7
|
+
class Keyword:
|
8
|
+
token: str
|
9
|
+
|
10
|
+
def is_bold(self) -> bool:
|
11
|
+
return self.token[0] in ["*", "_"] and len(self.token) == 2
|
12
|
+
|
13
|
+
def is_italic(self) -> bool:
|
14
|
+
return self.token[0] in ["*", "_"] and len(self.token) == 1
|
15
|
+
|
16
|
+
def is_bold_and_italic(self) -> bool:
|
17
|
+
return self.token[0] in ["*", "_"] and len(self.token) > 2
|
18
|
+
|
19
|
+
def is_bold_or_italic(self) -> bool:
|
20
|
+
return self.token[0] in ["*", "_"]
|
21
|
+
|
22
|
+
|
23
|
+
class InvalidState(Exception):
|
24
|
+
def __init__(self, token: str):
|
25
|
+
super().__init__(token)
|
26
|
+
self.token = token
|
27
|
+
|
28
|
+
|
29
|
+
@dataclass
|
30
|
+
class InlineScope:
|
31
|
+
state: State
|
32
|
+
|
33
|
+
stack: list[tuple[Keyword, State]] = field(default_factory=list)
|
34
|
+
|
35
|
+
@property
|
36
|
+
def last(self) -> str | None:
|
37
|
+
return self.stack[-1][0].token if len(self.stack) > 0 else None
|
38
|
+
|
39
|
+
@property
|
40
|
+
def strike(self) -> bool:
|
41
|
+
return self.last == "~~"
|
42
|
+
|
43
|
+
@property
|
44
|
+
def code(self) -> bool:
|
45
|
+
return self.last == "`"
|
46
|
+
|
47
|
+
def enter(self, kind: Keyword):
|
48
|
+
match kind:
|
49
|
+
case "`":
|
50
|
+
state = self.state._enter_scope(dim=True)
|
51
|
+
case "~~":
|
52
|
+
state = self.state._enter_scope(strike=True)
|
53
|
+
case _ if kind.is_bold():
|
54
|
+
state = self.state._enter_scope(bold=True)
|
55
|
+
case _ if kind.is_italic():
|
56
|
+
state = self.state._enter_scope(italic=True)
|
57
|
+
case _ if kind.is_bold_and_italic():
|
58
|
+
state = self.state._enter_scope(bold=True, italic=True)
|
59
|
+
case _:
|
60
|
+
state = self.state._enter_scope()
|
61
|
+
self.stack.append((kind, state))
|
62
|
+
|
63
|
+
def exit(self):
|
64
|
+
t = self.stack.pop()
|
65
|
+
state = t[1]
|
66
|
+
self.state._exit_scope(state)
|
67
|
+
|
68
|
+
|
69
|
+
class InlineTextPrinter:
|
70
|
+
def __init__(self, p: StreamedMarkdownPrinter, table: bool = False):
|
71
|
+
self.p = p
|
72
|
+
self.terminator = ["\n", None]
|
73
|
+
if table:
|
74
|
+
self.terminator.append("|")
|
75
|
+
|
76
|
+
def emit(self, s: str):
|
77
|
+
self.p.emit(s)
|
78
|
+
|
79
|
+
def peek(self):
|
80
|
+
return self.p.peek()
|
81
|
+
|
82
|
+
async def consume(self, n: int = 1):
|
83
|
+
return await self.p.consume(n)
|
84
|
+
|
85
|
+
async def check(self, s: str, eof: bool | None = None) -> bool:
|
86
|
+
return await self.p.check(s, eof)
|
87
|
+
|
88
|
+
async def parse_inline_unformatted(self):
|
89
|
+
# Parse until another "`" or a newline, or EOF
|
90
|
+
while not self.peek() in self.terminator:
|
91
|
+
c = await self.p.consume()
|
92
|
+
self.emit(c)
|
93
|
+
|
94
|
+
async def parse_inline_code(self):
|
95
|
+
self.emit("`")
|
96
|
+
# Parse until another "`" or a newline, or EOF
|
97
|
+
while not await self.check("`"):
|
98
|
+
c = await self.p.consume()
|
99
|
+
if c in self.terminator:
|
100
|
+
return
|
101
|
+
self.emit(c)
|
102
|
+
if c == "\\":
|
103
|
+
c = await self.p.consume()
|
104
|
+
if c in self.terminator:
|
105
|
+
return
|
106
|
+
self.emit(c)
|
107
|
+
self.emit("`")
|
108
|
+
await self.p.consume()
|
109
|
+
|
110
|
+
async def get_next_token(self) -> str | Keyword | None:
|
111
|
+
c = self.peek()
|
112
|
+
if c in self.terminator:
|
113
|
+
return None
|
114
|
+
if c == "`":
|
115
|
+
await self.consume()
|
116
|
+
return Keyword("`")
|
117
|
+
if c == "*":
|
118
|
+
s = ""
|
119
|
+
while self.peek() == "*":
|
120
|
+
s += c
|
121
|
+
await self.consume()
|
122
|
+
return Keyword(s)
|
123
|
+
if c == "_":
|
124
|
+
s = ""
|
125
|
+
while self.peek() == "_":
|
126
|
+
s += c
|
127
|
+
await self.consume()
|
128
|
+
return Keyword(s)
|
129
|
+
if c == "~" and await self.check("~~"):
|
130
|
+
s = ""
|
131
|
+
while self.peek() == "~":
|
132
|
+
s += c
|
133
|
+
await self.consume()
|
134
|
+
return Keyword(s)
|
135
|
+
if c == " " or c == "\t":
|
136
|
+
s = ""
|
137
|
+
while self.peek() == " " or self.peek() == "\t":
|
138
|
+
s += c
|
139
|
+
await self.consume()
|
140
|
+
return s
|
141
|
+
s = c or ""
|
142
|
+
if c == "\\":
|
143
|
+
await self.consume()
|
144
|
+
c = self.peek()
|
145
|
+
if c in self.terminator:
|
146
|
+
return None if len(s) == 0 else s
|
147
|
+
s += c or ""
|
148
|
+
await self.consume()
|
149
|
+
else:
|
150
|
+
await self.consume()
|
151
|
+
if len(s) == 0:
|
152
|
+
return None
|
153
|
+
return s
|
154
|
+
|
155
|
+
async def parse_inline(self, consume_trailing_newline: bool = True):
|
156
|
+
last_is_space = False
|
157
|
+
start = True
|
158
|
+
scope = InlineScope(self.p.state)
|
159
|
+
|
160
|
+
while True:
|
161
|
+
t = await self.get_next_token()
|
162
|
+
curr_is_space = False
|
163
|
+
|
164
|
+
if t is None:
|
165
|
+
if consume_trailing_newline:
|
166
|
+
if self.peek() == "\n":
|
167
|
+
await self.consume()
|
168
|
+
self.emit("\n")
|
169
|
+
return
|
170
|
+
elif isinstance(t, str):
|
171
|
+
# Space or normal text
|
172
|
+
if t[0] == " ":
|
173
|
+
curr_is_space = True
|
174
|
+
self.emit(" ")
|
175
|
+
else:
|
176
|
+
self.emit(t)
|
177
|
+
elif t.token == "`":
|
178
|
+
# Inline code
|
179
|
+
scope.enter(t)
|
180
|
+
with self.p.state.style(dim=True):
|
181
|
+
await self.parse_inline_code()
|
182
|
+
scope.exit()
|
183
|
+
elif t.token == "~~":
|
184
|
+
# Strike through
|
185
|
+
if not scope.strike:
|
186
|
+
scope.enter(t)
|
187
|
+
self.emit("~~")
|
188
|
+
else:
|
189
|
+
self.emit("~~")
|
190
|
+
scope.exit()
|
191
|
+
elif (
|
192
|
+
t.is_bold_or_italic()
|
193
|
+
and (last_is_space or start)
|
194
|
+
and scope.last != t.token
|
195
|
+
):
|
196
|
+
# Start bold or italics
|
197
|
+
scope.enter(t)
|
198
|
+
self.emit(t.token)
|
199
|
+
elif (
|
200
|
+
t.is_bold_or_italic()
|
201
|
+
and self.peek() in [" ", "\t", *self.terminator]
|
202
|
+
and scope.last == t.token
|
203
|
+
):
|
204
|
+
# End bold or italics
|
205
|
+
self.emit(t.token)
|
206
|
+
scope.exit()
|
207
|
+
else:
|
208
|
+
# print(
|
209
|
+
# "Invalid token:", t.token, f"[{self.peek()}]", scope.last == t.token
|
210
|
+
# )
|
211
|
+
raise InvalidState(t.token)
|
212
|
+
|
213
|
+
last_is_space = curr_is_space
|
214
|
+
start = False
|
autosh/md/printer.py
ADDED
@@ -0,0 +1,301 @@
|
|
1
|
+
import os
|
2
|
+
from typing import AsyncGenerator, Literal
|
3
|
+
import unicodedata
|
4
|
+
|
5
|
+
from autosh.md.state import Color, State
|
6
|
+
from autosh.md.stream import TextStream
|
7
|
+
|
8
|
+
|
9
|
+
class StreamedMarkdownPrinter:
|
10
|
+
def __init__(self, gen: AsyncGenerator[str, None]):
|
11
|
+
self.stream = TextStream(gen)
|
12
|
+
self.state = State()
|
13
|
+
|
14
|
+
def emit(self, s: str):
|
15
|
+
self.state.emit(s)
|
16
|
+
|
17
|
+
def peek(self):
|
18
|
+
return self.stream.peek()
|
19
|
+
|
20
|
+
async def consume(self, n: int = 1):
|
21
|
+
return await self.stream.consume(n)
|
22
|
+
|
23
|
+
async def check(self, s: str, eof: bool | None = None) -> bool:
|
24
|
+
return await self.stream.check(s, eof)
|
25
|
+
|
26
|
+
async def parse_inline(self, consume_trailing_newline: bool = True):
|
27
|
+
from autosh.md.inline_text import InlineTextPrinter
|
28
|
+
|
29
|
+
itp = InlineTextPrinter(self)
|
30
|
+
await itp.parse_inline(consume_trailing_newline)
|
31
|
+
|
32
|
+
async def parse_heading(self):
|
33
|
+
hashes = 0
|
34
|
+
while True:
|
35
|
+
c = await self.consume()
|
36
|
+
if c == "#":
|
37
|
+
hashes += 1
|
38
|
+
else:
|
39
|
+
break
|
40
|
+
match hashes:
|
41
|
+
case 1:
|
42
|
+
with self.state.style(bold=True, bg=Color.MAGENTA):
|
43
|
+
with self.state.style(dim=True):
|
44
|
+
self.emit("#" * hashes + " ")
|
45
|
+
await self.parse_inline(consume_trailing_newline=False)
|
46
|
+
case 2:
|
47
|
+
with self.state.style(bold=True, underline=True, color=Color.MAGENTA):
|
48
|
+
with self.state.style(dim=True):
|
49
|
+
self.emit("#" * hashes + " ")
|
50
|
+
await self.parse_inline(consume_trailing_newline=False)
|
51
|
+
case 3:
|
52
|
+
with self.state.style(bold=True, color=Color.MAGENTA):
|
53
|
+
with self.state.style(dim=True):
|
54
|
+
self.emit("#" * hashes + " ")
|
55
|
+
await self.parse_inline(consume_trailing_newline=False)
|
56
|
+
case 4:
|
57
|
+
with self.state.style(bold=True, italic=True, color=Color.MAGENTA):
|
58
|
+
with self.state.style(dim=True):
|
59
|
+
self.emit("#" * hashes + " ")
|
60
|
+
await self.parse_inline(consume_trailing_newline=False)
|
61
|
+
case _:
|
62
|
+
with self.state.style(bold=True):
|
63
|
+
with self.state.style(dim=True):
|
64
|
+
self.emit("#" * hashes + " ")
|
65
|
+
await self.parse_inline(consume_trailing_newline=False)
|
66
|
+
await self.consume() # consume the newline
|
67
|
+
self.emit("\n")
|
68
|
+
|
69
|
+
async def parse_paragraph(self):
|
70
|
+
while True:
|
71
|
+
await self.parse_inline()
|
72
|
+
if (
|
73
|
+
self.peek() != "\n"
|
74
|
+
and not await self.stream.non_paragraph_block_start()
|
75
|
+
):
|
76
|
+
if self.peek() == "\n":
|
77
|
+
await self.consume()
|
78
|
+
break
|
79
|
+
else:
|
80
|
+
break
|
81
|
+
|
82
|
+
async def parse_multiline_code(self):
|
83
|
+
with self.state.style(dim=True):
|
84
|
+
self.emit("```")
|
85
|
+
await self.consume(3)
|
86
|
+
while not await self.check("\n```"):
|
87
|
+
c = await self.consume()
|
88
|
+
if c is None:
|
89
|
+
self.emit("\n")
|
90
|
+
return
|
91
|
+
self.emit(c)
|
92
|
+
self.emit("\n```\n")
|
93
|
+
await self.consume(4)
|
94
|
+
|
95
|
+
async def parse_list(self, ordered: bool):
|
96
|
+
indents = [0]
|
97
|
+
counter = [1]
|
98
|
+
# first item
|
99
|
+
if ordered:
|
100
|
+
self.emit("1. ")
|
101
|
+
await self.consume()
|
102
|
+
else:
|
103
|
+
self.emit("• ")
|
104
|
+
await self.consume()
|
105
|
+
await self.parse_inline()
|
106
|
+
while True:
|
107
|
+
indent = 0
|
108
|
+
while self.peek() in [" ", "\t", "\n"]:
|
109
|
+
if self.peek() in [" ", "\t"]:
|
110
|
+
indent += 1
|
111
|
+
if self.peek() == "\n":
|
112
|
+
indent = 0
|
113
|
+
await self.consume()
|
114
|
+
if self.peek() is None:
|
115
|
+
return
|
116
|
+
if ordered and not await self.stream.ordered_list_label():
|
117
|
+
return
|
118
|
+
if not ordered and not await self.stream.unordered_list_label():
|
119
|
+
return
|
120
|
+
if not ordered:
|
121
|
+
await self.consume()
|
122
|
+
else:
|
123
|
+
while self.peek() is not None and self.peek() != ".":
|
124
|
+
await self.consume()
|
125
|
+
await self.consume()
|
126
|
+
|
127
|
+
depth = None
|
128
|
+
for i in range(len(indents) - 1):
|
129
|
+
if indents[i] <= indent and indents[i + 1] > indent:
|
130
|
+
depth = i
|
131
|
+
break
|
132
|
+
if depth is None and indents[-1] + 2 <= indent:
|
133
|
+
# indent one more level
|
134
|
+
indents.append(indent)
|
135
|
+
depth = len(indents) - 1
|
136
|
+
counter.append(1)
|
137
|
+
elif depth is None:
|
138
|
+
# same as last level
|
139
|
+
depth = len(indents) - 1
|
140
|
+
counter[depth] += 1
|
141
|
+
else:
|
142
|
+
# dedent
|
143
|
+
indents = indents[: depth + 1]
|
144
|
+
counter = counter[: depth + 1]
|
145
|
+
counter[depth] += 1
|
146
|
+
if not ordered:
|
147
|
+
self.emit(" " * depth + "• ")
|
148
|
+
else:
|
149
|
+
self.emit(" " * depth + str(counter[depth]) + ". ")
|
150
|
+
await self.parse_inline()
|
151
|
+
|
152
|
+
async def parse_blockquote(self):
|
153
|
+
from autosh.md.inline_text import InlineTextPrinter
|
154
|
+
|
155
|
+
while True:
|
156
|
+
while self.peek() in [" ", "\t"]:
|
157
|
+
await self.consume()
|
158
|
+
if self.peek() != ">":
|
159
|
+
break
|
160
|
+
await self.consume()
|
161
|
+
with self.state.style(bold=True, dim=True):
|
162
|
+
self.emit(">")
|
163
|
+
with self.state.style(dim=True):
|
164
|
+
itp = InlineTextPrinter(self)
|
165
|
+
await itp.parse_inline_unformatted()
|
166
|
+
if self.peek() == "\n":
|
167
|
+
self.emit("\n")
|
168
|
+
await self.consume()
|
169
|
+
|
170
|
+
async def parse_table(self):
|
171
|
+
def str_size(s: str) -> int:
|
172
|
+
size = 0
|
173
|
+
for c in s:
|
174
|
+
match unicodedata.east_asian_width(c):
|
175
|
+
case "F" | "W":
|
176
|
+
size += 2
|
177
|
+
case _:
|
178
|
+
size += 1
|
179
|
+
return size
|
180
|
+
|
181
|
+
rows: list[list[str]] = []
|
182
|
+
while True:
|
183
|
+
if self.peek() != "|":
|
184
|
+
break
|
185
|
+
s = "|"
|
186
|
+
await self.consume()
|
187
|
+
while self.peek() not in ["\n", None]:
|
188
|
+
s += self.peek() or ""
|
189
|
+
await self.consume()
|
190
|
+
if self.peek() == "\n":
|
191
|
+
await self.consume()
|
192
|
+
s = s.strip("|")
|
193
|
+
rows.append([c.strip() for c in s.split("|")])
|
194
|
+
# not enough rows
|
195
|
+
if len(rows) < 2:
|
196
|
+
self.emit("| " + " | ".join(rows[0]) + " |\n")
|
197
|
+
# do some simple formatting
|
198
|
+
cols = len(rows[0])
|
199
|
+
col_widths = [0] * cols
|
200
|
+
aligns: list[Literal["left", "right", "center"]] = ["left"] * cols
|
201
|
+
for i, row in enumerate(rows):
|
202
|
+
if i == 1:
|
203
|
+
# check for alignment
|
204
|
+
for j, c in enumerate(row):
|
205
|
+
if c.startswith(":") and c.endswith(":"):
|
206
|
+
aligns[j] = "center"
|
207
|
+
elif c.startswith(":"):
|
208
|
+
aligns[j] = "left"
|
209
|
+
elif c.endswith(":"):
|
210
|
+
aligns[j] = "right"
|
211
|
+
continue
|
212
|
+
for j, c in enumerate(row):
|
213
|
+
col_widths[j] = max(col_widths[j], str_size(c))
|
214
|
+
# print top border
|
215
|
+
with self.state.style(dim=True):
|
216
|
+
for j, c in enumerate(rows[0]):
|
217
|
+
self.emit("┌" if j == 0 else "┬")
|
218
|
+
self.emit("─" * (col_widths[j] + 2))
|
219
|
+
self.emit("┐\n")
|
220
|
+
# print the table
|
221
|
+
for i, row in enumerate(rows):
|
222
|
+
for j, c in enumerate(row):
|
223
|
+
with self.state.style(dim=True):
|
224
|
+
self.emit("│" if i != 1 else ("├" if j == 0 else "┼"))
|
225
|
+
if i == 1:
|
226
|
+
text = "─" * (col_widths[j] + 2)
|
227
|
+
else:
|
228
|
+
s = str_size(c)
|
229
|
+
align = aligns[j] if i != 0 else "center"
|
230
|
+
match align:
|
231
|
+
case "left":
|
232
|
+
text = c + " " * (col_widths[j] - s)
|
233
|
+
case "right":
|
234
|
+
text = " " * (col_widths[j] - s) + c
|
235
|
+
case "center":
|
236
|
+
padding = col_widths[j] - s
|
237
|
+
text = " " * (padding // 2) + c
|
238
|
+
text += " " * (padding - padding // 2)
|
239
|
+
text = f" {text} "
|
240
|
+
with self.state.style(dim=i == 1):
|
241
|
+
self.emit(text)
|
242
|
+
with self.state.style(dim=True):
|
243
|
+
self.emit("│\n" if i != 1 else "┤\n")
|
244
|
+
# print bottom border
|
245
|
+
with self.state.style(dim=True):
|
246
|
+
for j, c in enumerate(rows[0]):
|
247
|
+
self.emit("└" if j == 0 else "┴")
|
248
|
+
self.emit("─" * (col_widths[j] + 2))
|
249
|
+
self.emit("┘\n")
|
250
|
+
|
251
|
+
async def parse_doc(self):
|
252
|
+
await self.stream.init()
|
253
|
+
start = True
|
254
|
+
while True:
|
255
|
+
# Remove leading spaces and empty lines
|
256
|
+
indent = 0
|
257
|
+
while self.peek() in [" ", "\t", "\n"]:
|
258
|
+
if self.peek() in [" ", "\t"]:
|
259
|
+
indent += 1
|
260
|
+
if self.peek() == "\n":
|
261
|
+
indent = 0
|
262
|
+
await self.consume()
|
263
|
+
if self.peek() is None:
|
264
|
+
break
|
265
|
+
if not start:
|
266
|
+
self.emit("\n")
|
267
|
+
start = False
|
268
|
+
match c := self.peek():
|
269
|
+
case None:
|
270
|
+
break
|
271
|
+
# Heading
|
272
|
+
case "#":
|
273
|
+
await self.parse_heading()
|
274
|
+
# Code
|
275
|
+
case "`" if await self.check("```"):
|
276
|
+
await self.parse_multiline_code()
|
277
|
+
# Separator
|
278
|
+
case _ if await self.check("---"):
|
279
|
+
await self.consume(3)
|
280
|
+
width = min(os.get_terminal_size().columns, 80)
|
281
|
+
with self.state.style(dim=True):
|
282
|
+
self.emit("─" * width)
|
283
|
+
# Unordered list
|
284
|
+
case _ if await self.stream.unordered_list_label():
|
285
|
+
await self.parse_list(False)
|
286
|
+
# Ordered list
|
287
|
+
case _ if await self.stream.ordered_list_label():
|
288
|
+
await self.parse_list(True)
|
289
|
+
# Blockquote
|
290
|
+
case ">":
|
291
|
+
await self.parse_blockquote()
|
292
|
+
# Table
|
293
|
+
case "|":
|
294
|
+
await self.parse_table()
|
295
|
+
# Normal paragraph
|
296
|
+
case _:
|
297
|
+
await self.parse_paragraph()
|
298
|
+
self.emit("\x1b[0m") # Reset all
|
299
|
+
|
300
|
+
def __await__(self):
|
301
|
+
return self.parse_doc().__await__()
|
autosh/md/state.py
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from contextlib import contextmanager
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
HIGHLIGHT_COLOR_START = "35"
|
6
|
+
HIGHLIGHT_COLOR_END = "0"
|
7
|
+
ESC = "\x1b"
|
8
|
+
|
9
|
+
|
10
|
+
class Color(Enum):
|
11
|
+
BLACK = 30, 40
|
12
|
+
RED = 31, 41
|
13
|
+
GREEN = 32, 42
|
14
|
+
YELLOW = 33, 43
|
15
|
+
BLUE = 34, 44
|
16
|
+
MAGENTA = 35, 45
|
17
|
+
CYAN = 36, 46
|
18
|
+
WHITE = 37, 47
|
19
|
+
|
20
|
+
BRIGHT_BLACK = 90, 100
|
21
|
+
BRIGHT_RED = 91, 101
|
22
|
+
BRIGHT_GREEN = 92, 102
|
23
|
+
BRIGHT_YELLOW = 93, 103
|
24
|
+
BRIGHT_BLUE = 94, 104
|
25
|
+
BRIGHT_MAGENTA = 95, 105
|
26
|
+
BRIGHT_CYAN = 96, 106
|
27
|
+
BRIGHT_WHITE = 97, 107
|
28
|
+
|
29
|
+
def __init__(self, foreground: int, background: int):
|
30
|
+
self.foreground = foreground
|
31
|
+
self.background = background
|
32
|
+
|
33
|
+
|
34
|
+
@dataclass
|
35
|
+
class State:
|
36
|
+
is_bold: bool = False
|
37
|
+
is_dim: bool = False
|
38
|
+
is_italic: bool = False
|
39
|
+
is_underline: bool = False
|
40
|
+
is_strike: bool = False
|
41
|
+
foreground_color: Color | None = None
|
42
|
+
background_color: Color | None = None
|
43
|
+
|
44
|
+
def emit(self, text: str):
|
45
|
+
print(text, end="", flush=True)
|
46
|
+
|
47
|
+
def _enter_scope(
|
48
|
+
self,
|
49
|
+
bold: bool | None = None,
|
50
|
+
dim: bool | None = None,
|
51
|
+
italic: bool | None = None,
|
52
|
+
underline: bool | None = None,
|
53
|
+
strike: bool | None = None,
|
54
|
+
color: Color | None = None,
|
55
|
+
bg: Color | None = None,
|
56
|
+
) -> "State":
|
57
|
+
old_state = State(
|
58
|
+
is_bold=self.is_bold,
|
59
|
+
is_dim=self.is_dim,
|
60
|
+
is_italic=self.is_italic,
|
61
|
+
is_underline=self.is_underline,
|
62
|
+
is_strike=self.is_strike,
|
63
|
+
foreground_color=self.foreground_color,
|
64
|
+
background_color=self.background_color,
|
65
|
+
)
|
66
|
+
if bold is not None:
|
67
|
+
self.is_bold = bold
|
68
|
+
if dim is not None:
|
69
|
+
self.is_dim = dim
|
70
|
+
if italic is not None:
|
71
|
+
self.is_italic = italic
|
72
|
+
if underline is not None:
|
73
|
+
self.is_underline = underline
|
74
|
+
if strike is not None:
|
75
|
+
self.is_strike = strike
|
76
|
+
if color is not None:
|
77
|
+
self.foreground_color = color
|
78
|
+
if bg is not None:
|
79
|
+
self.background_color = bg
|
80
|
+
self.__apply_all()
|
81
|
+
return old_state
|
82
|
+
|
83
|
+
def _exit_scope(self, old_state: "State"):
|
84
|
+
self.is_bold = old_state.is_bold
|
85
|
+
self.is_dim = old_state.is_dim
|
86
|
+
self.is_italic = old_state.is_italic
|
87
|
+
self.is_underline = old_state.is_underline
|
88
|
+
self.is_strike = old_state.is_strike
|
89
|
+
self.foreground_color = old_state.foreground_color
|
90
|
+
self.background_color = old_state.background_color
|
91
|
+
self.__apply_all()
|
92
|
+
|
93
|
+
@contextmanager
|
94
|
+
def style(
|
95
|
+
self,
|
96
|
+
bold: bool | None = None,
|
97
|
+
dim: bool | None = None,
|
98
|
+
italic: bool | None = None,
|
99
|
+
underline: bool | None = None,
|
100
|
+
strike: bool | None = None,
|
101
|
+
color: Color | None = None,
|
102
|
+
bg: Color | None = None,
|
103
|
+
):
|
104
|
+
old_state = self._enter_scope(
|
105
|
+
bold=bold,
|
106
|
+
dim=dim,
|
107
|
+
italic=italic,
|
108
|
+
underline=underline,
|
109
|
+
strike=strike,
|
110
|
+
color=color,
|
111
|
+
bg=bg,
|
112
|
+
)
|
113
|
+
yield
|
114
|
+
self._exit_scope(old_state)
|
115
|
+
|
116
|
+
def __apply_all(self):
|
117
|
+
self.emit(f"{ESC}[0m")
|
118
|
+
codes = []
|
119
|
+
if self.is_bold:
|
120
|
+
codes.append(1)
|
121
|
+
if self.is_dim:
|
122
|
+
codes.append(2)
|
123
|
+
if self.is_italic:
|
124
|
+
codes.append(3)
|
125
|
+
if self.is_underline:
|
126
|
+
codes.append(4)
|
127
|
+
if self.is_strike:
|
128
|
+
codes.append(9)
|
129
|
+
if self.foreground_color:
|
130
|
+
codes.append(self.foreground_color.foreground)
|
131
|
+
if self.background_color:
|
132
|
+
codes.append(self.background_color.background)
|
133
|
+
|
134
|
+
codes = ";".join(map(str, codes))
|
135
|
+
if len(codes) > 0:
|
136
|
+
self.emit(f"{ESC}[{codes}m")
|
autosh/md/stream.py
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
from typing import AsyncGenerator
|
2
|
+
|
3
|
+
|
4
|
+
class TextStream:
|
5
|
+
def __init__(self, gen: AsyncGenerator[str, None]):
|
6
|
+
self.__stream = gen
|
7
|
+
self.__buf: str = ""
|
8
|
+
self.__eof = False
|
9
|
+
|
10
|
+
async def init(self):
|
11
|
+
try:
|
12
|
+
self.__buf = await anext(self.__stream)
|
13
|
+
except StopAsyncIteration:
|
14
|
+
self.__eof = True
|
15
|
+
|
16
|
+
async def __ensure_length(self, n: int):
|
17
|
+
while (not self.__eof) and len(self.__buf) < n:
|
18
|
+
try:
|
19
|
+
self.__buf += await anext(self.__stream)
|
20
|
+
except StopAsyncIteration:
|
21
|
+
self.__eof = True
|
22
|
+
|
23
|
+
def peek(self):
|
24
|
+
c = self.__buf[0] if len(self.__buf) > 0 else None
|
25
|
+
return c
|
26
|
+
|
27
|
+
async def check(self, s: str, eof: bool | None = None) -> bool:
|
28
|
+
if len(s) == 0:
|
29
|
+
return True
|
30
|
+
await self.__ensure_length(len(s) + 1)
|
31
|
+
if len(self.__buf) < len(s):
|
32
|
+
return False
|
33
|
+
matched = self.__buf[0 : len(s)] == s
|
34
|
+
if matched:
|
35
|
+
if eof is not None:
|
36
|
+
if eof:
|
37
|
+
# return false if there is more data
|
38
|
+
if len(self.__buf) > len(s):
|
39
|
+
return False
|
40
|
+
else:
|
41
|
+
# return false if there is no more data
|
42
|
+
if len(self.__buf) == len(s):
|
43
|
+
return False
|
44
|
+
return matched
|
45
|
+
|
46
|
+
async def consume(self, n: int = 1):
|
47
|
+
await self.__ensure_length(n)
|
48
|
+
if len(self.__buf) < n:
|
49
|
+
raise ValueError("Not enough data to consume")
|
50
|
+
s = self.__buf[:n]
|
51
|
+
self.__buf = self.__buf[n:]
|
52
|
+
await self.__ensure_length(1)
|
53
|
+
return s
|
54
|
+
|
55
|
+
async def unordered_list_label(self) -> bool:
|
56
|
+
if self.__eof:
|
57
|
+
return False
|
58
|
+
await self.__ensure_length(2)
|
59
|
+
buf = self.__buf
|
60
|
+
if len(buf) < 2:
|
61
|
+
return False
|
62
|
+
if buf[0] in ["-", "+", "*"] and buf[1] == " ":
|
63
|
+
return True
|
64
|
+
return False
|
65
|
+
|
66
|
+
async def ordered_list_label(self) -> bool:
|
67
|
+
if self.__eof:
|
68
|
+
return False
|
69
|
+
await self.__ensure_length(5)
|
70
|
+
buf = self.__buf
|
71
|
+
# \d+\.
|
72
|
+
if len(buf) == 0:
|
73
|
+
return False
|
74
|
+
if not buf[0].isnumeric():
|
75
|
+
return False
|
76
|
+
has_dot = False
|
77
|
+
for i in range(1, 5):
|
78
|
+
if i >= len(buf):
|
79
|
+
return False
|
80
|
+
c = buf[i]
|
81
|
+
if c == ".":
|
82
|
+
if has_dot:
|
83
|
+
return False
|
84
|
+
has_dot = True
|
85
|
+
continue
|
86
|
+
if c == " ":
|
87
|
+
if has_dot:
|
88
|
+
return True
|
89
|
+
return False
|
90
|
+
if c.isnumeric():
|
91
|
+
continue
|
92
|
+
return False
|
93
|
+
|
94
|
+
async def non_paragraph_block_start(self):
|
95
|
+
await self.__ensure_length(3)
|
96
|
+
buf = self.__buf[:3] if len(self.__buf) >= 3 else self.__buf
|
97
|
+
if buf.startswith("```"):
|
98
|
+
return True
|
99
|
+
if buf.startswith("---"):
|
100
|
+
return True
|
101
|
+
if buf.startswith("> "):
|
102
|
+
return True
|
103
|
+
if await self.ordered_list_label():
|
104
|
+
return True
|
105
|
+
if await self.unordered_list_label():
|
106
|
+
return True
|
107
|
+
return False
|
autosh/plugins/__init__.py
CHANGED
@@ -29,8 +29,8 @@ def confirm(message: str):
|
|
29
29
|
|
30
30
|
|
31
31
|
def cmd_preview_panel(title: str, content: RenderableType, short: str | None = None):
|
32
|
-
if CLI_OPTIONS.quiet
|
33
|
-
if short:
|
32
|
+
if CLI_OPTIONS.quiet:
|
33
|
+
if short and not CLI_OPTIONS.yes:
|
34
34
|
rich.print(f"[magenta]{short}[/magenta]\n")
|
35
35
|
return
|
36
36
|
panel = Panel.fit(content, title=f"[magenta]{title}[/magenta]", title_align="left")
|
autosh/session.py
CHANGED
@@ -88,7 +88,7 @@ class Session:
|
|
88
88
|
def _get_argv_message(self):
|
89
89
|
args = str(CLI_OPTIONS.args)
|
90
90
|
if not CLI_OPTIONS.script:
|
91
|
-
cmd =
|
91
|
+
cmd = Path(sys.argv[0]).name
|
92
92
|
else:
|
93
93
|
cmd = CLI_OPTIONS.script.name
|
94
94
|
return UserMessage(
|
@@ -116,7 +116,21 @@ class Session:
|
|
116
116
|
if CLI_OPTIONS.stdin_has_data():
|
117
117
|
self.agent.history.add(
|
118
118
|
UserMessage(
|
119
|
-
content="IMPORTANT:
|
119
|
+
content="IMPORTANT: You are acting as an intermediate tool of a workflow. Input data is fed to you through piped stdin. Please use tools to read when necessary.",
|
120
|
+
role="user",
|
121
|
+
)
|
122
|
+
)
|
123
|
+
if not sys.stdout.isatty():
|
124
|
+
self.agent.history.add(
|
125
|
+
UserMessage(
|
126
|
+
content="IMPORTANT: You are acting as an intermediate tool of a workflow. Your output should only contain the user expected output, nothing else. Don't ask user questions or print anything else since the user cannot see it.",
|
127
|
+
role="user",
|
128
|
+
)
|
129
|
+
)
|
130
|
+
else:
|
131
|
+
self.agent.history.add(
|
132
|
+
UserMessage(
|
133
|
+
content="IMPORTANT: This is a one-off run, so don't ask user questions since the user cannot reply.",
|
120
134
|
role="user",
|
121
135
|
)
|
122
136
|
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: autosh
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.3
|
4
4
|
Summary: Add your description here
|
5
5
|
License-File: LICENSE
|
6
6
|
Requires-Python: >=3.13
|
@@ -31,7 +31,9 @@ As an interactive shell: `ash` (alternatively, `autosh`)
|
|
31
31
|
|
32
32
|
Execute a single prompt: `ash "list current directory"`
|
33
33
|
|
34
|
-
Process piped data:
|
34
|
+
Process piped data:
|
35
|
+
* `cat README.md | ash -y "summarise"`
|
36
|
+
* `cat in.csv | ash -y -q "double the first numeric column" > out.csv`
|
35
37
|
|
36
38
|
## Scripting
|
37
39
|
|
@@ -72,6 +74,8 @@ Write "Hello, world" to _test.log
|
|
72
74
|
|
73
75
|
# TODO
|
74
76
|
|
75
|
-
- [ ] Image generation
|
76
|
-
- [ ] Image input
|
77
|
+
- [ ] Image input, generation, and editing
|
77
78
|
- [ ] RAG for non-text files
|
79
|
+
- [ ] Plugin system
|
80
|
+
- [ ] MCP support
|
81
|
+
- [ ] A better input widget with history and auto completion
|
@@ -0,0 +1,22 @@
|
|
1
|
+
autosh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
autosh/config-template.toml,sha256=iLdCBHIK0czWgNHtwAgvuhV670aiNc-IOmaPHP12i0Y,269
|
3
|
+
autosh/config.py,sha256=7DXUSsqHm8dPsXln-NTu8D0_Uwcs2h4sT8pFtQM8tso,2654
|
4
|
+
autosh/main.py,sha256=ov8lBOUb7GOa3-WBW00gqKaYrTG3NFYQgIXP3-6Eki4,4975
|
5
|
+
autosh/session.py,sha256=pU3AAqI3vEEFdtsXK_1ZV6uXzjkzIG8q3Yo01K-Q6dw,9615
|
6
|
+
autosh/md/__init__.py,sha256=0SK4rkynUwR77E8IU-ORJmWou_aTxC6xzQQo-IfAsaM,213
|
7
|
+
autosh/md/inline_text.py,sha256=I8CaxksvOpTr1mlUtnk50TVVgF9CLnLsth2ux0cdTM4,6422
|
8
|
+
autosh/md/printer.py,sha256=qD3AcGosnPMRAf-KiRw1PUixtP-kS5xFIPFjzGLLT0g,11099
|
9
|
+
autosh/md/state.py,sha256=OEp0kUb63HrAN5BPcPx9mxWogofayumH9GZGqLdOoEA,3792
|
10
|
+
autosh/md/stream.py,sha256=zaVJR6Kog6EZcuVaGszwgywpysHcn8fRZ0KItmKRhiA,3224
|
11
|
+
autosh/plugins/__init__.py,sha256=NXq27wvS97NEH6hTtWRyp-ut0BE9fq5QyKBjXOj39zY,2487
|
12
|
+
autosh/plugins/calc.py,sha256=qo0EajIpNPv9PtLNLygyEjVaxo1F6_S62kmoJZq5oLM,581
|
13
|
+
autosh/plugins/cli.py,sha256=D6S_QHPmjBBB9gwgXeJrwxUs3u0TNty_tHVICbEPGbs,8522
|
14
|
+
autosh/plugins/clock.py,sha256=GGi0HAG6f6-FP1qqGoyCcUj11q_VnkaGArumsMk0CkY,542
|
15
|
+
autosh/plugins/code.py,sha256=0JwFzq6ejgbisCqBm_RG1r1WEVNou64ue-siVIpvZqs,2291
|
16
|
+
autosh/plugins/search.py,sha256=1d3Gqq6uXu0ntTBpw44Ab_haAySvZLMj3e2MQd3DHO0,2736
|
17
|
+
autosh/plugins/web.py,sha256=lmD2JnsqVI1qKgSFrk39851jCZoPyPRaVvHeEFYXylA,2597
|
18
|
+
autosh-0.0.3.dist-info/METADATA,sha256=M2wVPjOWoiCXWbYzs216oQ4eUSE_s5-b67-qTufmnRE,1897
|
19
|
+
autosh-0.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
20
|
+
autosh-0.0.3.dist-info/entry_points.txt,sha256=BV7bzUnxG6Z5InEkrfajGCxjooYORC5tZDDZctOPenQ,67
|
21
|
+
autosh-0.0.3.dist-info/licenses/LICENSE,sha256=BnLDJsIJe-Dm18unR9DOoSv7QOfAz6LeIQc1yHAjxp0,1066
|
22
|
+
autosh-0.0.3.dist-info/RECORD,,
|
autosh/md.py
DELETED
@@ -1,408 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
from typing import AsyncGenerator, Literal
|
3
|
-
|
4
|
-
|
5
|
-
HIGHLIGHT_COLOR_START = "35"
|
6
|
-
HIGHLIGHT_COLOR_END = "0"
|
7
|
-
|
8
|
-
|
9
|
-
class MarkdowmPrinter:
|
10
|
-
def __init__(self, stream: AsyncGenerator[str, None]):
|
11
|
-
async def char_stream(stream: AsyncGenerator[str, None]):
|
12
|
-
async for chunk in stream:
|
13
|
-
for char in chunk:
|
14
|
-
yield char
|
15
|
-
|
16
|
-
self.stream = char_stream(stream)
|
17
|
-
self.__buf: str = ""
|
18
|
-
self.__eof = False
|
19
|
-
|
20
|
-
def peek(self):
|
21
|
-
c = self.__buf[0] if len(self.__buf) > 0 else None
|
22
|
-
return c
|
23
|
-
|
24
|
-
async def __ensure_length(self, n: int):
|
25
|
-
while (not self.__eof) and len(self.__buf) < n:
|
26
|
-
try:
|
27
|
-
self.__buf += await self.stream.__anext__()
|
28
|
-
except StopAsyncIteration:
|
29
|
-
self.__eof = True
|
30
|
-
|
31
|
-
async def __check_unordered_list_label(self) -> bool:
|
32
|
-
if self.__eof:
|
33
|
-
return False
|
34
|
-
await self.__ensure_length(2)
|
35
|
-
buf = self.__buf
|
36
|
-
if len(buf) < 2:
|
37
|
-
return False
|
38
|
-
if buf[0] in ["-", "+", "*"] and buf[1] == " ":
|
39
|
-
return True
|
40
|
-
return False
|
41
|
-
|
42
|
-
async def __check_ordered_list_label(self) -> bool:
|
43
|
-
if self.__eof:
|
44
|
-
return False
|
45
|
-
await self.__ensure_length(5)
|
46
|
-
buf = self.__buf
|
47
|
-
# \d+\.
|
48
|
-
if len(buf) == 0:
|
49
|
-
return False
|
50
|
-
if not buf[0].isnumeric():
|
51
|
-
return False
|
52
|
-
has_dot = False
|
53
|
-
for i in range(1, 5):
|
54
|
-
if i >= len(buf):
|
55
|
-
return False
|
56
|
-
c = buf[i]
|
57
|
-
if c == ".":
|
58
|
-
if has_dot:
|
59
|
-
return False
|
60
|
-
has_dot = True
|
61
|
-
continue
|
62
|
-
if c == " ":
|
63
|
-
if has_dot:
|
64
|
-
return True
|
65
|
-
return False
|
66
|
-
if c.isnumeric():
|
67
|
-
continue
|
68
|
-
return False
|
69
|
-
|
70
|
-
async def check(self, s: str):
|
71
|
-
if len(s) == 0:
|
72
|
-
return True
|
73
|
-
await self.__ensure_length(len(s))
|
74
|
-
if len(self.__buf) < len(s):
|
75
|
-
return False
|
76
|
-
return self.__buf[0 : len(s)] == s
|
77
|
-
|
78
|
-
async def check_non_paragraph_block_start(self):
|
79
|
-
await self.__ensure_length(3)
|
80
|
-
buf = self.__buf[:3] if len(self.__buf) >= 3 else self.__buf
|
81
|
-
if buf.startswith("```"):
|
82
|
-
return True
|
83
|
-
if buf.startswith("---"):
|
84
|
-
return True
|
85
|
-
if buf.startswith("> "):
|
86
|
-
return True
|
87
|
-
if await self.__check_ordered_list_label():
|
88
|
-
return True
|
89
|
-
if await self.__check_unordered_list_label():
|
90
|
-
return True
|
91
|
-
return False
|
92
|
-
|
93
|
-
async def next(self):
|
94
|
-
c = self.__buf[0] if len(self.__buf) > 0 else None
|
95
|
-
self.__buf = self.__buf[1:] if len(self.__buf) > 0 else ""
|
96
|
-
if c is None:
|
97
|
-
return None
|
98
|
-
if not self.__eof:
|
99
|
-
try:
|
100
|
-
self.__buf += await self.stream.__anext__()
|
101
|
-
except StopAsyncIteration:
|
102
|
-
self.__eof = True
|
103
|
-
return c
|
104
|
-
|
105
|
-
def print(self, s: str):
|
106
|
-
print(s, end="", flush=True)
|
107
|
-
|
108
|
-
async def parse_single_line_text(
|
109
|
-
self,
|
110
|
-
outer_is_italic: bool = False,
|
111
|
-
outer_is_bold: bool = False,
|
112
|
-
outer_is_dim: bool = False,
|
113
|
-
):
|
114
|
-
styles: list[Literal["code", "bold", "italic", "strike"]] = []
|
115
|
-
|
116
|
-
def find(s: Literal["code", "bold", "italic", "strike"]):
|
117
|
-
for i in range(len(styles) - 1, -1, -1):
|
118
|
-
if styles[i] == s:
|
119
|
-
return i
|
120
|
-
return None
|
121
|
-
|
122
|
-
def find_italic_first():
|
123
|
-
for i in range(len(styles) - 1, -1, -1):
|
124
|
-
if styles[i] == "bold":
|
125
|
-
return False
|
126
|
-
if styles[i] == "italic":
|
127
|
-
return True
|
128
|
-
return False
|
129
|
-
|
130
|
-
# Remove leading spaces
|
131
|
-
while self.peek() in [" ", "\t"]:
|
132
|
-
await self.next()
|
133
|
-
|
134
|
-
while True:
|
135
|
-
not_code = find("code") is None
|
136
|
-
c = self.peek()
|
137
|
-
if c == "\n" or c is None:
|
138
|
-
await self.next()
|
139
|
-
self.print("\x1b[0m\n") # Reset all and newline
|
140
|
-
return
|
141
|
-
match c:
|
142
|
-
case "`":
|
143
|
-
await self.next()
|
144
|
-
if (i := find("code")) is not None:
|
145
|
-
self.print(c)
|
146
|
-
if not outer_is_dim:
|
147
|
-
self.print("\x1b[22m")
|
148
|
-
if find("bold") is not None or outer_is_bold:
|
149
|
-
self.print("\x1b[1m")
|
150
|
-
styles = styles[:i]
|
151
|
-
else:
|
152
|
-
self.print("\x1b[2m")
|
153
|
-
styles.append("code")
|
154
|
-
self.print(c)
|
155
|
-
# Bold
|
156
|
-
case "*" if (
|
157
|
-
not_code and await self.check("**") and not find_italic_first()
|
158
|
-
):
|
159
|
-
await self.next()
|
160
|
-
await self.next()
|
161
|
-
# print(">", styles, find("bold"))
|
162
|
-
if (i := find("bold")) is not None:
|
163
|
-
self.print(c)
|
164
|
-
self.print(c)
|
165
|
-
if not outer_is_bold:
|
166
|
-
self.print("\x1b[22m")
|
167
|
-
styles = styles[:i]
|
168
|
-
else:
|
169
|
-
self.print("\x1b[1m")
|
170
|
-
styles.append("bold")
|
171
|
-
self.print(c)
|
172
|
-
self.print(c)
|
173
|
-
case "_" if (
|
174
|
-
not_code and await self.check("__") and not find_italic_first()
|
175
|
-
):
|
176
|
-
await self.next()
|
177
|
-
await self.next()
|
178
|
-
if (i := find("bold")) is not None:
|
179
|
-
self.print(c)
|
180
|
-
self.print(c)
|
181
|
-
if not outer_is_bold:
|
182
|
-
self.print("\x1b[22m")
|
183
|
-
styles = styles[:i]
|
184
|
-
else:
|
185
|
-
self.print("\x1b[1m")
|
186
|
-
styles.append("bold")
|
187
|
-
self.print(c)
|
188
|
-
self.print(c)
|
189
|
-
# Italic
|
190
|
-
case "*" | "_" if (
|
191
|
-
not_code
|
192
|
-
and not await self.check("* ")
|
193
|
-
and not await self.check("_ ")
|
194
|
-
):
|
195
|
-
await self.next()
|
196
|
-
if (i := find("italic")) is not None:
|
197
|
-
self.print(c)
|
198
|
-
if not outer_is_italic:
|
199
|
-
self.print("\x1b[23m")
|
200
|
-
styles = styles[:i]
|
201
|
-
# print(styles, await self.check("**"))
|
202
|
-
else:
|
203
|
-
self.print("\x1b[3m")
|
204
|
-
styles.append("italic")
|
205
|
-
self.print(c)
|
206
|
-
# Strike through
|
207
|
-
case "~" if not_code and await self.check("~~"):
|
208
|
-
await self.next()
|
209
|
-
await self.next()
|
210
|
-
if (i := find("strike")) is not None:
|
211
|
-
self.print("~~")
|
212
|
-
self.print("\x1b[29m")
|
213
|
-
styles = styles[:i]
|
214
|
-
else:
|
215
|
-
self.print("\x1b[9m")
|
216
|
-
styles.append("strike")
|
217
|
-
self.print("~~")
|
218
|
-
case _:
|
219
|
-
self.print(c)
|
220
|
-
await self.next()
|
221
|
-
|
222
|
-
async def parse_heading(self):
|
223
|
-
hashes = 0
|
224
|
-
while True:
|
225
|
-
c = await self.next()
|
226
|
-
if c == "#":
|
227
|
-
hashes += 1
|
228
|
-
else:
|
229
|
-
break
|
230
|
-
# Start control
|
231
|
-
match hashes:
|
232
|
-
case 1:
|
233
|
-
self.print("\x1b[45;1;2m") # Magenta background, bold, dim
|
234
|
-
self.print("#" * hashes)
|
235
|
-
self.print(" \x1b[22;1m") # Reset dim
|
236
|
-
await self.parse_single_line_text(outer_is_bold=True)
|
237
|
-
case 2:
|
238
|
-
self.print("\x1b[35;1;2;4m") # Magenta foreground, bold, dim, underline
|
239
|
-
self.print("#" * hashes)
|
240
|
-
self.print(" \x1b[22m\x1b[1m") # Reset dim
|
241
|
-
await self.parse_single_line_text(outer_is_bold=True)
|
242
|
-
case 3:
|
243
|
-
self.print("\x1b[35;1;2m") # Magenta foreground, bold, dim
|
244
|
-
self.print("#" * hashes)
|
245
|
-
self.print(" \x1b[22m\x1b[1m") # Reset dim
|
246
|
-
await self.parse_single_line_text(outer_is_bold=True)
|
247
|
-
case 4:
|
248
|
-
self.print("\x1b[35;2;3m") # Magenta foreground, dim, italic
|
249
|
-
self.print("#" * hashes)
|
250
|
-
self.print(" \x1b[22m") # Reset dim
|
251
|
-
await self.parse_single_line_text(outer_is_italic=True)
|
252
|
-
case _:
|
253
|
-
self.print("\x1b[2m") # dim
|
254
|
-
self.print("#" * hashes)
|
255
|
-
self.print(" \x1b[22m") # Reset dim
|
256
|
-
await self.parse_single_line_text()
|
257
|
-
# Stream title
|
258
|
-
|
259
|
-
async def parse_paragraph(self):
|
260
|
-
while True:
|
261
|
-
await self.parse_single_line_text()
|
262
|
-
if self.peek() != "\n" and not await self.check_non_paragraph_block_start():
|
263
|
-
await self.next()
|
264
|
-
break
|
265
|
-
else:
|
266
|
-
break
|
267
|
-
|
268
|
-
async def parse_multiline_code(self):
|
269
|
-
# dim
|
270
|
-
self.print("\x1b[2m")
|
271
|
-
self.print("```")
|
272
|
-
await self.next()
|
273
|
-
await self.next()
|
274
|
-
await self.next()
|
275
|
-
while not await self.check("\n```"):
|
276
|
-
c = await self.next()
|
277
|
-
if c is None:
|
278
|
-
self.print("\n")
|
279
|
-
return
|
280
|
-
self.print(c)
|
281
|
-
self.print("\n```\n")
|
282
|
-
await self.next()
|
283
|
-
await self.next()
|
284
|
-
await self.next()
|
285
|
-
await self.next()
|
286
|
-
|
287
|
-
async def parse_list(self, ordered: bool):
|
288
|
-
indents = [0]
|
289
|
-
counter = [1]
|
290
|
-
# first item
|
291
|
-
if ordered:
|
292
|
-
self.print("1. ")
|
293
|
-
await self.next()
|
294
|
-
else:
|
295
|
-
self.print("• ")
|
296
|
-
await self.next()
|
297
|
-
await self.parse_single_line_text()
|
298
|
-
while True:
|
299
|
-
indent = 0
|
300
|
-
while self.peek() in [" ", "\t", "\n"]:
|
301
|
-
if self.peek() in [" ", "\t"]:
|
302
|
-
indent += 1
|
303
|
-
if self.peek() == "\n":
|
304
|
-
indent = 0
|
305
|
-
await self.next()
|
306
|
-
if self.peek() is None:
|
307
|
-
return
|
308
|
-
if ordered and not await self.__check_ordered_list_label():
|
309
|
-
return
|
310
|
-
if not ordered and not await self.__check_unordered_list_label():
|
311
|
-
return
|
312
|
-
if not ordered:
|
313
|
-
await self.next()
|
314
|
-
else:
|
315
|
-
while self.peek() is not None and self.peek() != ".":
|
316
|
-
await self.next()
|
317
|
-
await self.next()
|
318
|
-
|
319
|
-
depth = None
|
320
|
-
for i in range(len(indents) - 1):
|
321
|
-
if indents[i] <= indent and indents[i + 1] > indent:
|
322
|
-
depth = i
|
323
|
-
break
|
324
|
-
if depth is None and indents[-1] + 2 <= indent:
|
325
|
-
# indent one more level
|
326
|
-
indents.append(indent)
|
327
|
-
depth = len(indents) - 1
|
328
|
-
counter.append(1)
|
329
|
-
elif depth is None:
|
330
|
-
# same as last level
|
331
|
-
depth = len(indents) - 1
|
332
|
-
counter[depth] += 1
|
333
|
-
else:
|
334
|
-
# dedent
|
335
|
-
indents = indents[: depth + 1]
|
336
|
-
counter = counter[: depth + 1]
|
337
|
-
counter[depth] += 1
|
338
|
-
if not ordered:
|
339
|
-
self.print(" " * depth + "• ")
|
340
|
-
else:
|
341
|
-
self.print(" " * depth + str(counter[depth]) + ". ")
|
342
|
-
await self.parse_single_line_text()
|
343
|
-
|
344
|
-
async def parse_blockquote(self):
|
345
|
-
while True:
|
346
|
-
while self.peek() in [" ", "\t"]:
|
347
|
-
await self.next()
|
348
|
-
if self.peek() != ">":
|
349
|
-
break
|
350
|
-
await self.next()
|
351
|
-
self.print("\x1b[1;2m|\x1b[22;2m ")
|
352
|
-
await self.parse_single_line_text(outer_is_dim=True)
|
353
|
-
|
354
|
-
async def parse_doc(self):
|
355
|
-
self.__buf = await self.stream.__anext__()
|
356
|
-
start = True
|
357
|
-
while True:
|
358
|
-
# Remove leading spaces and empty lines
|
359
|
-
indent = 0
|
360
|
-
while self.peek() in [" ", "\t", "\n"]:
|
361
|
-
if self.peek() in [" ", "\t"]:
|
362
|
-
indent += 1
|
363
|
-
if self.peek() == "\n":
|
364
|
-
indent = 0
|
365
|
-
await self.next()
|
366
|
-
if self.peek() is None:
|
367
|
-
break
|
368
|
-
if not start:
|
369
|
-
self.print("\n")
|
370
|
-
start = False
|
371
|
-
match c := self.peek():
|
372
|
-
case None:
|
373
|
-
break
|
374
|
-
# Heading
|
375
|
-
case "#":
|
376
|
-
await self.parse_heading()
|
377
|
-
# Code
|
378
|
-
case "`" if await self.check("```"):
|
379
|
-
await self.parse_multiline_code()
|
380
|
-
# Separator
|
381
|
-
case _ if await self.check("---"):
|
382
|
-
await self.next()
|
383
|
-
await self.next()
|
384
|
-
await self.next()
|
385
|
-
width = min(os.get_terminal_size().columns, 80)
|
386
|
-
self.print("\x1b[2m" + "─" * width + "\x1b[22m\n")
|
387
|
-
# Unordered list
|
388
|
-
case _ if await self.__check_unordered_list_label():
|
389
|
-
await self.parse_list(False)
|
390
|
-
# Ordered list
|
391
|
-
case _ if await self.__check_ordered_list_label():
|
392
|
-
await self.parse_list(True)
|
393
|
-
# Blockquote
|
394
|
-
case ">":
|
395
|
-
await self.parse_blockquote()
|
396
|
-
# Normal paragraph
|
397
|
-
case _:
|
398
|
-
await self.parse_paragraph()
|
399
|
-
self.print("\x1b[0m\x1b[0m\x1b[0m") # Reset all
|
400
|
-
self.print("\x1b[0m") # Reset all
|
401
|
-
|
402
|
-
def __await__(self):
|
403
|
-
return self.parse_doc().__await__()
|
404
|
-
|
405
|
-
|
406
|
-
async def stream_md(stream: AsyncGenerator[str, None]):
|
407
|
-
mp = MarkdowmPrinter(stream)
|
408
|
-
await mp.parse_doc()
|
autosh-0.0.2.dist-info/RECORD
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
autosh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
autosh/config.py,sha256=KPuXr_MF5Pbdn5pCCgq9AcyDSDlOnCJ4vBetKmhodic,2348
|
3
|
-
autosh/main.py,sha256=myRolKqfHyQEgujqmUndDSm9J0C21w3zqA9-SYNX1kY,4961
|
4
|
-
autosh/md.py,sha256=qXg5ZFVUwek3rUXb-oEti1soRRbIq8EbDq88lEYRoO4,14267
|
5
|
-
autosh/session.py,sha256=kCaD-7tZpozz56sWlMGp0njqwYRf3cZOau1ulWRKJEI,8885
|
6
|
-
autosh/plugins/__init__.py,sha256=yOTobuyYFpUWl5BCowGzJG8rZX2Whlagail_QyHVlo4,2487
|
7
|
-
autosh/plugins/calc.py,sha256=qo0EajIpNPv9PtLNLygyEjVaxo1F6_S62kmoJZq5oLM,581
|
8
|
-
autosh/plugins/cli.py,sha256=D6S_QHPmjBBB9gwgXeJrwxUs3u0TNty_tHVICbEPGbs,8522
|
9
|
-
autosh/plugins/clock.py,sha256=GGi0HAG6f6-FP1qqGoyCcUj11q_VnkaGArumsMk0CkY,542
|
10
|
-
autosh/plugins/code.py,sha256=0JwFzq6ejgbisCqBm_RG1r1WEVNou64ue-siVIpvZqs,2291
|
11
|
-
autosh/plugins/search.py,sha256=1d3Gqq6uXu0ntTBpw44Ab_haAySvZLMj3e2MQd3DHO0,2736
|
12
|
-
autosh/plugins/web.py,sha256=lmD2JnsqVI1qKgSFrk39851jCZoPyPRaVvHeEFYXylA,2597
|
13
|
-
autosh-0.0.2.dist-info/METADATA,sha256=y9pbK71nuueA11LCv6AsR24MSsnqNut-7-zljpOC5_Y,1720
|
14
|
-
autosh-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
-
autosh-0.0.2.dist-info/entry_points.txt,sha256=BV7bzUnxG6Z5InEkrfajGCxjooYORC5tZDDZctOPenQ,67
|
16
|
-
autosh-0.0.2.dist-info/licenses/LICENSE,sha256=BnLDJsIJe-Dm18unR9DOoSv7QOfAz6LeIQc1yHAjxp0,1066
|
17
|
-
autosh-0.0.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|