Browse Source

Added Basic Input and Terminal

pull/1/head
Eugenio Parodi 5 years ago
parent
commit
7199cfd7a9
  1. 1
      docs/HLD.md
  2. 11
      docs/TODO.md
  3. 2
      docs/hld.svg
  4. 0
      tests/test.draw.py
  5. 39
      tests/test.input.py
  6. 4
      tests/utf-8.txt
  7. 1
      ttk/__init__.py
  8. 2
      ttk/libbpytop/__init__.py
  9. 198
      ttk/libbpytop/input.py
  10. 83
      ttk/libbpytop/term.py
  11. 72
      ttk/log.py

1
docs/HLD.md

@ -1 +1,2 @@
# HLD
![diagram](hld.svg)

11
docs/TODO.md

@ -1,10 +1,15 @@
- [ ] Handle Logs
- [ ] Terminal Helper
- [ ] Events
- [x] Window : SIGWINCH triggered when the terminal is resized
- [ ] Input Class
- [ ] Return Error if Mouse RE does not match
- [x] Handle the Paste Buffer
- [ ] Handle the middle button mouse paste
- [ ] Handle Special Keys (UP, Down, . . .)
- [ ] Events
https://tkinterexamples.com/events/events.html
https://www.pythontutorial.net/tkinter/tkinter-event-binding/
- [ ] Keyboard
- [ ] Mouse
- [ ] Window
- [x] Keyboard
- [x] Mouse
- [ ] Canvas Class

2
docs/hld.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 16 KiB

0
tests/test.draw.py

39
tests/test.input.py

@ -0,0 +1,39 @@
import sys, os
import logging
sys.path.append(os.path.join(sys.path[0],'..'))
import ttk.libbpytop as lbt
import ttk
def message_handler(mode, context, message):
if mode == ttk.InfoMsg: mode = 'INFO'
elif mode == ttk.WarningMsg: mode = 'WARNING'
elif mode == ttk.CriticalMsg: mode = 'CRITICAL'
elif mode == ttk.FatalMsg: mode = 'FATAL'
else: mode = 'DEBUG'
logging.debug(f"{mode} {context.file} {message}")
logging.basicConfig(level=logging.DEBUG,
format='(%(threadName)-9s) %(message)s',)
ttk.installMessageHandler(message_handler)
ttk.info("Retrieve Keyboard, Mouse press/drag/wheel Events")
ttk.info("Press q or <ESC> to exit")
lbt.Term.push(lbt.Term.mouse_on)
lbt.Term.echo(False)
def keyCallback(kevt=None, mevt=None):
if kevt is not None:
ttk.info(f"Key Event: {kevt}")
if mevt is not None:
ttk.info(f"Mouse Event: {mevt}")
def winCallback(width, height):
ttk.info(f"Resize: w:{width}, h:{height}")
lbt.Term.registerResizeCb(winCallback)
lbt.Input.get_key(keyCallback)
lbt.Term.push(lbt.Term.mouse_off, lbt.Term.mouse_direct_off)
lbt.Term.echo(True)

4
tests/utf-8.txt

@ -0,0 +1,4 @@
This is a testing file with some UTF-8 chars:
x x x x x x x x x x x x
£ @ £ ¬ ` 漢 _ _ あ _ _
x x x x x xx x x xx x x

1
ttk/__init__.py

@ -0,0 +1 @@
from .log import *

2
ttk/libbpytop/__init__.py

@ -0,0 +1,2 @@
from .input import *
from .term import *

198
ttk/libbpytop/input.py

@ -0,0 +1,198 @@
#!/usr/bin/env python3
# Copyright 2021 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
# Copyright 2020 Aristocratos (https://github.com/aristocratos/bpytop)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os, sys, io, threading, signal, re, subprocess, logging, logging.handlers, argparse
import re
import queue
from select import select
from time import time, sleep, strftime, localtime
from typing import TextIO, List, Set, Dict, Tuple, Optional, Union, Any, Callable, ContextManager, Iterable, Type, NamedTuple
try: import fcntl, termios, tty, pwd
except Exception as e:
print(f'ERROR: {e}')
exit(1)
class MouseEvent:
# Keys
NoButton = 0x00000000 # The button state does not refer to any button (see QMouseEvent::button()).
AllButtons = 0x07ffffff # This value corresponds to a mask of all possible mouse buttons. Use to set the 'acceptedButtons' property of a MouseArea to accept ALL mouse buttons.
LeftButton = 0x00000001 # The left button is pressed, or an event refers to the left button. (The left button may be the right button on left-handed mice.)
RightButton = 0x00000002 # The right button.
MidButton = 0x00000004 # The middle button.
MiddleButton = MidButton # The middle button.
Wheel = 0x00000008
# Events
NoEvent = 0x00000000
Press = 0x00010000
Release = 0x00020000
Drag = 0x00040000
Move = 0x00080000
Up = 0x00100000 # Wheel Up
Down = 0x00200000 # Wheel Down
__slots__ = ('x','y','key','evt','raw')
def __init__(self, x: int, y: int, key: int, evt: int, raw: str):
self.x = x
self.y = y
self.key = key
self.evt = evt
self.raw = raw
def key2str(self):
return {
MouseEvent.NoButton : "NoButton",
MouseEvent.AllButtons : "AllButtons",
MouseEvent.LeftButton : "LeftButton",
MouseEvent.RightButton : "RightButton",
MouseEvent.MidButton : "MidButton",
MouseEvent.MiddleButton : "MiddleButton",
MouseEvent.Wheel : "Wheel",
}.get(self.key, "Undefined")
def evt2str(self):
return {
MouseEvent.NoEvent : "NoEvent",
MouseEvent.Press : "Press",
MouseEvent.Release : "Release",
MouseEvent.Drag : "Drag",
MouseEvent.Move : "Move",
MouseEvent.Up : "Up",
MouseEvent.Down : "Down",
}.get(self.evt, "Undefined")
def __str__(self):
return f"MouseEvent ({self.x},{self.y}) {self.key2str()} {self.evt2str()} - {self.raw}"
class KeyEvent:
__slots__ = ('id', 'key')
def __init__(self, id:int, key: str):
self.id = id
self.key = key
def __str__(self):
return f"KeyEvent: {self.key}"
class Input:
class _nonblocking(object):
"""Set nonblocking mode for device"""
def __init__(self, stream: TextIO):
self.stream = stream
self.fd = self.stream.fileno()
def __enter__(self):
self.orig_fl = fcntl.fcntl(self.fd, fcntl.F_GETFL)
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl | os.O_NONBLOCK)
def __exit__(self, *args):
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl)
class _raw(object):
"""Set raw input mode for device"""
def __init__(self, stream: TextIO):
self.stream = stream
self.fd = self.stream.fileno()
def __enter__(self):
self.original_stty = termios.tcgetattr(self.stream)
tty.setcbreak(self.stream)
def __exit__(self, type, value, traceback):
termios.tcsetattr(self.stream, termios.TCSANOW, self.original_stty)
@staticmethod
def get_key(callback=None):
input_key: str = ""
mouse_re = re.compile(r"\033\[<(\d+);(\d+);(\d+)([mM])")
while not False:
with Input._raw(sys.stdin):
#if not select([sys.stdin], [], [], 0.1)[0]: #* Wait 100ms for input on stdin then restart loop to check for stop flag
# continue
input_key += sys.stdin.read(1) #* Read 1 key safely with blocking on
if input_key == "\033": #* If first character is a escape sequence keep reading
with Input._nonblocking(sys.stdin): #* Set non blocking to prevent read stall
input_key += sys.stdin.read(20)
if input_key.startswith("\033[<"):
_ = sys.stdin.read(1000)
# ttk.debug("INPUT: "+input_key.replace("\033","<ESC>"))
mevt = None
kevt = None
if input_key == "\033" or input_key == "q": #* Key is "escape" key if only containing \033
break
elif len(input_key) == 1:
# Key Pressed
kevt = KeyEvent(0, input_key)
elif input_key.startswith("\033[<"):
# Mouse Event
m = mouse_re.match(input_key)
if not m:
# TODO: Return Error
continue
code = int(m.group(1))
x = int(m.group(2))
y = int(m.group(3))
state = m.group(4)
key = MouseEvent.NoButton
evt = MouseEvent.NoEvent
if code == 0x00:
key = MouseEvent.LeftButton
evt = MouseEvent.Press if state=="M" else MouseEvent.Release
elif code == 0x01:
key = MouseEvent.MidButton
evt = MouseEvent.Press if state=="M" else MouseEvent.Release
elif code == 0x02:
key = MouseEvent.RightButton
evt = MouseEvent.Press if state=="M" else MouseEvent.Release
elif code == 0x20:
key = MouseEvent.LeftButton
evt = MouseEvent.Drag
elif code == 0x21:
key = MouseEvent.MidButton
evt = MouseEvent.Drag
elif code == 0x22:
key = MouseEvent.RightButton
evt = MouseEvent.Drag
elif code == 0x40:
key = MouseEvent.Wheel
evt = MouseEvent.Up
elif code == 0x41:
key = MouseEvent.Wheel
evt = MouseEvent.Down
mevt = MouseEvent(x, y, key, evt, m.group(0).replace("\033", "<ESC>"))
input_key = ""
if callback is not None:
callback(kevt, mevt)
def main():
print("Retrieve Keyboard, Mouse press/drag/wheel Events")
print("Press q or <ESC> to exit")
import term as t
t.Term.push(t.Term.mouse_on)
t.Term.echo(False)
def callback(kevt=None, mevt=None):
if kevt is not None:
print(f"Key Event: {kevt}")
if mevt is not None:
print(f"Mouse Event: {mevt}")
Input.get_key(callback)
t.Term.push(t.Term.mouse_off, t.Term.mouse_direct_off)
t.Term.echo(True)
if __name__ == "__main__":
main()

83
ttk/libbpytop/term.py

@ -0,0 +1,83 @@
#!/usr/bin/env python3
# Copyright 2021 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
# Copyright 2020 Aristocratos (https://github.com/aristocratos/bpytop)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os, sys, io, threading, signal, re, subprocess, logging, logging.handlers, argparse
import queue
from select import select
from time import time, sleep, strftime, localtime
from typing import List, Set, Dict, Tuple, Optional, Union, Any, Callable, ContextManager, Iterable, Type, NamedTuple
try: import fcntl, termios, tty, pwd
except Exception as e:
print(f'ERROR: {e}')
exit(1)
class Term:
"""Terminal info and commands"""
width: int = 0
height: int = 0
fg: str = "" #* Default foreground color
bg: str = "" #* Default background color
hide_cursor = "\033[?25l" #* Hide terminal cursor
show_cursor = "\033[?25h" #* Show terminal cursor
alt_screen = "\033[?1049h" #* Switch to alternate screen
normal_screen = "\033[?1049l" #* Switch to normal screen
clear = "\033[2J\033[0;0f" #* Clear screen and set cursor to position 0,0
mouse_on = "\033[?1002h\033[?1015h\033[?1006h" #* Enable reporting of mouse position on click and release
mouse_off = "\033[?1002l" #* Disable mouse reporting
mouse_direct_on = "\033[?1003h" #* Enable reporting of mouse position at any movement
mouse_direct_off = "\033[?1003l" #* Disable direct mouse reporting
_sigWinChCb = None
@staticmethod
def echo(on: bool):
"""Toggle input echo"""
(iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(sys.stdin.fileno())
if on:
lflag |= termios.ECHO # type: ignore
else:
lflag &= ~termios.ECHO # type: ignore
new_attr = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, new_attr)
@staticmethod
def push(*args):
try:
print(*args, sep="", end="", flush=True)
except BlockingIOError:
pass
print(*args, sep="", end="", flush=True)
@staticmethod
def title(text: str = "") -> str:
out: str = f'{os.environ.get("TERMINAL_TITLE", "")}'
if out and text: out += " "
if text: out += f'{text}'
return f'\033]0;{out}\a'
@staticmethod
def _sigWinCh(signum, frame):
Term.width, Term.height = os.get_terminal_size()
if Term._sigWinChCb is not None:
Term._sigWinChCb(Term.width, Term.height)
@staticmethod
def registerResizeCb(callback):
Term._sigWinChCb = callback
signal.signal(signal.SIGWINCH, Term._sigWinCh)

72
ttk/log.py

@ -0,0 +1,72 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2021 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# This code is inspired by
# https://github.com/ceccopierangiolieugenio/pyCuT/blob/master/cupy/CuTCore/CuDebug.py
import inspect
DebugMsg = 0 # A message generated by the Debug() function.
InfoMsg = 4 # A message generated by the Info() function.
WarningMsg = 1 # A message generated by the Warning() function.
CriticalMsg = 2 # A message generated by the Critical() function.
FatalMsg = 3 # A message generated by the Fatal() function.
SystemMsg = CriticalMsg
_MessageHandler = None
def _process_msg(mode, msg):
global _MessageHandler
if _MessageHandler is not None:
curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe,1)
if len(calframe) > 2:
class context:
__slots__ = ('file', 'line', 'function')
def __str__(self):
return f"{self.file}:{self.line} [{self.function}]"
ctx = context()
ctx.file = calframe[2][1]
ctx.line = calframe[2][2]
ctx.function = calframe[2][3]
_MessageHandler(mode, ctx, msg)
def debug(msg):
_process_msg(DebugMsg, msg)
def info(msg):
_process_msg(InfoMsg, msg)
def warn(msg):
_process_msg(WarningMsg, msg)
def critical(msg):
_process_msg(CriticalMsg, msg)
def fatal(msg):
_process_msg(FatalMsg, msg)
def installMessageHandler(mh):
global _MessageHandler
_MessageHandler = mh
Loading…
Cancel
Save