support color output in console
This commit is contained in:
parent
3b7338eb79
commit
82f59676c7
3 changed files with 81 additions and 41 deletions
94
app/main.py
94
app/main.py
|
|
@ -4,8 +4,10 @@ import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import errno
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import pty
|
||||||
import random
|
import random
|
||||||
import shlex
|
import shlex
|
||||||
import time
|
import time
|
||||||
|
|
@ -603,46 +605,80 @@ class Main:
|
||||||
await self.sio.emit('cli_close', {'exitcode': 0})
|
await self.sio.emit('cli_close', {'exitcode': 0})
|
||||||
return
|
return
|
||||||
|
|
||||||
proc = None
|
|
||||||
try:
|
try:
|
||||||
async def _read_stream(streamType, stream):
|
|
||||||
while True:
|
|
||||||
line = await stream.readline()
|
|
||||||
if line:
|
|
||||||
await self.sio.emit('cli_output', {
|
|
||||||
'type': streamType,
|
|
||||||
'line': line.decode('utf-8')
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
LOG.info(f'CLI: {data}')
|
LOG.info(f'CLI: {data}')
|
||||||
|
|
||||||
args = ['yt-dlp', *shlex.split(data)]
|
args = ['yt-dlp'] + shlex.split(data)
|
||||||
|
_env = os.environ.copy()
|
||||||
|
_env.update({
|
||||||
|
"TERM": "xterm-256color",
|
||||||
|
"LANG": "en_US.UTF-8",
|
||||||
|
"SHELL": "/bin/bash",
|
||||||
|
"LC_ALL": "en_US.UTF-8",
|
||||||
|
"PWD": self.config.download_path,
|
||||||
|
"FORCE_COLOR": "1",
|
||||||
|
"PYTHONUNBUFFERED": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
master_fd, slave_fd = pty.openpty()
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
*args,
|
*args,
|
||||||
cwd=self.config.download_path,
|
cwd=self.config.download_path,
|
||||||
stdin=asyncio.subprocess.DEVNULL,
|
stdin=asyncio.subprocess.DEVNULL,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=slave_fd,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=slave_fd,
|
||||||
env=os.environ.copy().update({
|
env=_env,
|
||||||
"TERM": "xterm-256color",
|
|
||||||
"LANG": "en_US.UTF-8",
|
|
||||||
"SHELL": "/bin/bash",
|
|
||||||
"LC_ALL": "en_US.UTF-8",
|
|
||||||
"PWD": self.config.download_path,
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncio.gather(
|
try:
|
||||||
_read_stream('stdout', proc.stdout),
|
os.close(slave_fd)
|
||||||
_read_stream('stderr', proc.stderr),
|
except Exception as e:
|
||||||
)
|
LOG.error(f'Error closing PTY: {str(e)}')
|
||||||
|
|
||||||
while proc.returncode is None:
|
async def read_pty():
|
||||||
await asyncio.sleep(0.1)
|
loop = asyncio.get_running_loop()
|
||||||
|
buffer = b''
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = await loop.run_in_executor(None, lambda: os.read(master_fd, 1024))
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.EIO:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
await self.sio.emit('cli_close', {'exitcode': proc.returncode})
|
if not chunk:
|
||||||
|
# No more output
|
||||||
|
if buffer:
|
||||||
|
# Emit any remaining partial line
|
||||||
|
await self.sio.emit('cli_output', {
|
||||||
|
'type': 'stdout',
|
||||||
|
'line': buffer.decode('utf-8', errors='replace')
|
||||||
|
})
|
||||||
|
break
|
||||||
|
buffer += chunk
|
||||||
|
*lines, buffer = buffer.split(b'\n')
|
||||||
|
for line in lines:
|
||||||
|
await self.sio.emit('cli_output', {
|
||||||
|
'type': 'stdout',
|
||||||
|
'line': line.decode('utf-8', errors='replace')
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
os.close(master_fd)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f'Error closing PTY: {str(e)}')
|
||||||
|
|
||||||
|
# Start reading output from PTY
|
||||||
|
read_task = asyncio.create_task(read_pty())
|
||||||
|
|
||||||
|
# Wait until process finishes
|
||||||
|
returncode = await proc.wait()
|
||||||
|
|
||||||
|
# Ensure reading is done
|
||||||
|
await read_task
|
||||||
|
|
||||||
|
await self.sio.emit('cli_close', {'exitcode': returncode})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f'CLI Error: {str(e)}')
|
LOG.error(f'CLI Error: {str(e)}')
|
||||||
await self.sio.emit('cli_output', {
|
await self.sio.emit('cli_output', {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
<span class="icon"><font-awesome-icon icon="fa-solid fa-terminal" /></span> Terminal
|
<span class="icon"><font-awesome-icon icon="fa-solid fa-terminal" /></span> Terminal
|
||||||
</p>
|
</p>
|
||||||
<p class="card-header-icon">
|
<p class="card-header-icon">
|
||||||
<span v-tooltip.top="'Clear console window'" class="icon" @click="clearOutput"><font-awesome-icon icon="fa-solid fa-broom" /></span>
|
<span v-tooltip.top="'Clear console window'" class="icon" @click="clearOutput"><font-awesome-icon
|
||||||
|
icon="fa-solid fa-broom" /></span>
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<section class="card-content p-0 m-0">
|
<section class="card-content p-0 m-0">
|
||||||
|
|
@ -23,16 +24,23 @@
|
||||||
</section>
|
</section>
|
||||||
<section class="card-content p-1 m-1">
|
<section class="card-content p-1 m-1">
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control is-expanded has-icons-left">
|
<div class="control">
|
||||||
|
<span class="icon-text input is-unselectable">
|
||||||
|
<span class="icon"><font-awesome-icon icon="fa-solid fa-terminal" /></span>
|
||||||
|
<span>yt-dlp</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="control is-expanded">
|
||||||
<input type="text" class="input" v-model="command" placeholder="ls -la" autocomplete="off"
|
<input type="text" class="input" v-model="command" placeholder="ls -la" autocomplete="off"
|
||||||
ref="command_input" @keydown.enter="runCommand" :disabled="props.isLoading" id="command">
|
ref="command_input" @keydown.enter="runCommand" :disabled="props.isLoading" id="command">
|
||||||
<span class="icon is-left"><font-awesome-icon icon="fa-solid fa-terminal"
|
|
||||||
:spin="props.isLoading" /></span>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button is-primary" type="button" :disabled="props.isLoading || '' === command"
|
<button class="button is-primary" type="button" :disabled="props.isLoading || '' === command"
|
||||||
@click="runCommand">
|
@click="runCommand">
|
||||||
<span class="icon"><font-awesome-icon icon="fa-solid fa-paper-plane" /></span>
|
<span class="icon">
|
||||||
|
<font-awesome-icon icon="fa-solid fa-spinner" spin v-if="props.isLoading" />
|
||||||
|
<font-awesome-icon icon="fa-solid fa-paper-plane" v-else />
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -132,7 +140,8 @@ const runCommand = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter('runCommand', command.value)
|
emitter('runCommand', command.value)
|
||||||
terminal.value.writeln(`~ ${command.value}`)
|
terminal.value.writeln(`user@YTPTube ~`)
|
||||||
|
terminal.value.writeln(`$ yt-dlp ${command.value}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearOutput = async (withCommand = false) => {
|
const clearOutput = async (withCommand = false) => {
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import {
|
import {
|
||||||
faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload, faUpRightFromSquare,
|
faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload, faUpRightFromSquare,
|
||||||
faSpinner, faArrowUp, faArrowDown, faTasks, faCalendar, faArrowUpAZ, faArrowDownAZ, faEject, faGlobe, faMoon, faSun,
|
faSpinner, faArrowUp, faArrowDown, faTasks, faCalendar, faArrowUpAZ, faArrowDownAZ, faEject, faGlobe, faMoon, faSun,
|
||||||
faTerminal,
|
faTerminal, faBroom, faServer, faPowerOff, faPaperPlane, faBoxArchive,
|
||||||
faBroom,
|
|
||||||
faServer,
|
|
||||||
faPowerOff,
|
|
||||||
faPaperPlane,
|
|
||||||
faBoxArchive
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import { faSquare, faSquareCheck } from '@fortawesome/free-regular-svg-icons'
|
import { faSquare, faSquareCheck } from '@fortawesome/free-regular-svg-icons'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue