Merge pull request #138 from LazoCoder/terminal-adapter

Implement automatic discovery of terminal adapters
This commit is contained in:
Samuel Henrique 2018-01-07 15:22:30 -02:00 committed by GitHub
commit b304c7f357
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 179 additions and 160 deletions

View file

@ -178,7 +178,7 @@ Alternatively, you can delete images from this folder and it will not break the
$ sudo apt-get update
$ sudo apt install terminology
```
* If you get the error `39:46: syntax error: Expected end of line but found identifier. (-2741)`: Locate the file `ITerm.py` in `pokemonterminal/adapter/implementations` and on line 7, change `iTerm` to `iTerm2`. If you still experience the error, try changing it to `iTerm 2`.
* If you get the error `39:46: syntax error: Expected end of line but found identifier. (-2741)`: Locate the file `ITerm.py` in `pokemonterminal/terminal/adapters` and on line 9, change `iTerm` to `iTerm2`. If you still experience the error, try changing it to `iTerm 2`.
## Saving

View file

@ -1,24 +0,0 @@
from pokemonterminal.adapter.implementations.ITerm import ITerm
from pokemonterminal.adapter.implementations.NullAdapter import NullAdapter
from pokemonterminal.adapter.implementations.Terminology import Terminology
from pokemonterminal.adapter.implementations.Tilix import Tilix
available_terminals = [
Terminology,
Tilix,
ITerm
]
def identify():
"""
Identify the terminal we are using based on env vars.
:return: A terminal adapter interface or a NullAdapter.
:rtype: TerminalAdapterInterface
"""
for terminal in available_terminals:
if terminal.is_available():
return terminal()
return NullAdapter()

View file

@ -1,22 +0,0 @@
class TerminalAdapterInterface(object):
@staticmethod
def is_available():
"""
:return: True if the environment implies we are using this terminal.
:rtype bool
"""
raise NotImplementedError()
def set_image_file_path(self, image_file_path):
"""
Set the background image of the terminal.
:param image_file_path: Path to an image file.
:rtype str
"""
raise NotImplementedError()
def clear(self):
"""
Clear the terminal's background image.
"""
raise NotImplementedError()

View file

@ -1,33 +0,0 @@
import os
import subprocess
from pokemonterminal.adapter.base import TerminalAdapterInterface
# OSA script that will change the terminal background image
osa_script_fmt = """tell application "iTerm"
\ttell current session of current window
\t\tset background image to "{}"
\tend tell
end tell"""
class ITerm(TerminalAdapterInterface):
@staticmethod
def is_available():
return os.environ.get("ITERM_PROFILE")
def __run_osascript(self, stream):
p = subprocess.Popen(['osascript'], stdout=subprocess.PIPE,
stdin=subprocess.PIPE)
p.stdin.write(stream)
p.communicate()
p.stdin.close()
def set_image_file_path(self, image_file_path):
stdin = osa_script_fmt.format(image_file_path)
self.__run_osascript(str.encode(stdin))
def clear(self):
stdin = osa_script_fmt.format("")
self.__run_osascript(str.encode(stdin))

View file

@ -1,15 +0,0 @@
from pokemonterminal.adapter.base import TerminalAdapterInterface
class NullAdapter(TerminalAdapterInterface):
err = "This terminal emulator is not supported."
@staticmethod
def is_available():
return True
def set_image_file_path(self, image_file_path):
print(self.err)
def clear(self):
print(self.err)

View file

@ -1,15 +0,0 @@
import os
from pokemonterminal.adapter.base import TerminalAdapterInterface
class Terminology(TerminalAdapterInterface):
@staticmethod
def is_available():
return os.environ.get("TERMINOLOGY") == '1'
def set_image_file_path(self, image_file_path):
os.system('tybg "{}"'.format(image_file_path))
def clear(self):
os.system("tybg")

View file

@ -1,23 +1,55 @@
# Used for creating, running and analyzing applescript and bash scripts.
import sys
from pokemonterminal.adapter import identify
from .wallpaper import get_current_adapters
from .terminal import get_current_terminal_adapters
from .wallpaper import get_current_wallpaper_adapters
TERMINAL_PROVIDER = None
WALLPAPER_PROVIDER = None
def __init_terminal_provider():
global TERMINAL_PROVIDER
if TERMINAL_PROVIDER is not None:
return
providers = get_current_terminal_adapters()
if len(providers) > 1:
# All this if is really not supposed to happen at all whatsoever
# really what kind of person has 2 simultaneous T.E???
print("Multiple providers found select the appropriate one:")
for i, x in enumerate(providers):
print(f'{i}. {x.__str__()}')
print("If some of these make no sense or are irrelevant please file " +
"an issue in https://github.com/LazoCoder/Pokemon-Terminal")
print("=> ", end='')
inp = None
while inp is None:
try:
inp = int(input())
if inp >= len(providers):
raise ValueError()
except ValueError as _:
print("Invalid number, try again!")
TERMINAL_PROVIDER = providers[inp]
elif len(providers) <= 0:
print("Your terminal emulator isn't supported at this time.")
sys.exit()
else:
TERMINAL_PROVIDER = providers[0]
def __init_wallpaper_provider():
global WALLPAPER_PROVIDER
if WALLPAPER_PROVIDER is not None:
return
providers = get_current_adapters()
providers = get_current_wallpaper_adapters()
if len(providers) > 1:
# All this if is really not supposed to happen at all whatsoever
# really what kind of person has 2 simultaneous D.E???
print("Multiple providers found select the appropriate one:")
[print(str(x)) for x in providers]
print("If some of these make no sense or are irrelevant please file" +
for i, x in enumerate(providers):
print(f'{i}. {x.__str__()}')
print("If some of these make no sense or are irrelevant please file " +
"an issue in https://github.com/LazoCoder/Pokemon-Terminal")
print("=> ", end='')
inp = None
@ -37,18 +69,16 @@ def __init_wallpaper_provider():
def clear_terminal():
adapter = identify()
adapter.clear()
__init_terminal_provider()
TERMINAL_PROVIDER.clear()
def change_terminal(image_file_path):
if not isinstance(image_file_path, str):
print("A image path must be passed to the change terminal function.")
return
adapter = identify()
if adapter is None:
print("Terminal not supported")
adapter.set_image_file_path(image_file_path)
__init_terminal_provider()
TERMINAL_PROVIDER.change_terminal(image_file_path)
def change_wallpaper(image_file_path):

View file

@ -0,0 +1,34 @@
import os
import importlib
import inspect
from .adapters import TerminalProvider
def _is_adapter(member) -> bool:
return (inspect.isclass(member)
and issubclass(member, TerminalProvider)
and member != TerminalProvider)
def _get_adapter_classes() -> [TerminalProvider]:
"""
This methods reads all the modules in the adapters folder searching for
all the implementing wallpaper adapter classes
thanks for/adapted from https://github.com/cclauss/adapter_pattern/
"""
dirname = os.path.join(os.path.dirname(
os.path.abspath(__file__)), 'adapters')
adapter_classes = []
for file_name in sorted(os.listdir(dirname)):
root, ext = os.path.splitext(file_name)
if ext.lower() == '.py' and not root.startswith('__'):
module = importlib.import_module(
'.' + root, 'pokemonterminal.terminal.adapters')
for _, c in inspect.getmembers(module, _is_adapter):
adapter_classes.append(c)
return adapter_classes
def get_current_terminal_adapters() -> [TerminalProvider]:
arr = _get_adapter_classes()
return [x for x in arr if x.is_compatible()]

View file

@ -0,0 +1,27 @@
class TerminalProvider:
"""
Interface representing all the different terminal emulators supported
by pokemon-terminal if you want to implement a TE, create a module in this
folder that implements this interface, reflection will do the rest.
"""
def change_terminal(path: str):
"""
This sets the wallpaper of the corresponding TE of this adapter.
:param path The full path of the required pokemon image
"""
raise NotImplementedError()
def is_compatible() -> bool:
"""
checks for compatibility
:return a boolean saying whether or not the current adaptor is
compatible with the running TE
"""
raise NotImplementedError()
def clear(self):
"""
Clear the terminal's background image.
"""
raise NotImplementedError()

View file

@ -0,0 +1,34 @@
import os
import subprocess
from . import TerminalProvider as _TProv
class ItermProvider(_TProv):
# OSA script that will change the terminal background image
osa_script_fmt = """tell application "iTerm"
\ttell current session of current window
\t\tset background image to "{}"
\tend tell
end tell"""
def is_compatible() -> bool:
return os.environ.get("ITERM_PROFILE")
def __run_osascript(stream):
p = subprocess.Popen(['osascript'], stdout=subprocess.PIPE,
stdin=subprocess.PIPE)
p.stdin.write(stream)
p.communicate()
p.stdin.close()
def change_terminal(self, path: str):
stdin = self.osa_script_fmt.format(path)
self.__run_osascript(str.encode(stdin))
def clear(self):
stdin = self.osa_script_fmt.format("")
self.__run_osascript(str.encode(stdin))
def __str__():
return "iTerm 2"

View file

@ -0,0 +1,17 @@
import os
from . import TerminalProvider as _TProv
class TerminologyProvider(_TProv):
def is_compatible() -> bool:
return os.environ.get("TERMINOLOGY") == '1'
def change_terminal(path: str):
os.system('tybg "{}"'.format(path))
def clear():
os.system("tybg")
def __str__():
return "Terminology"

View file

@ -1,23 +1,25 @@
import os
from pokemonterminal.adapter.base import TerminalAdapterInterface
from . import TerminalProvider as _TProv
class Tilix(TerminalAdapterInterface):
class TilixProvider(_TProv):
setting_key = "com.gexperts.Tilix.Settings"
setting_field = "background-image"
@staticmethod
def is_available():
def is_compatible() -> bool:
return "TILIX_ID" in os.environ
def set_image_file_path(self, image_file_path):
def change_terminal(self, path: str):
command = 'gsettings set {} {} "{}"'
os.system(command.format(self.setting_key,
self.setting_field,
image_file_path))
path))
def clear(self):
command = 'gsettings set {} {}'
os.system(command.format(self.setting_key,
self.setting_field))
def __str__():
return "Tilix"

View file

@ -29,6 +29,6 @@ def _get_adapter_classes() -> [WallpaperProvider]:
return adapter_classes
def get_current_adapters() -> [WallpaperProvider]:
def get_current_wallpaper_adapters() -> [WallpaperProvider]:
arr = _get_adapter_classes()
return [x for x in arr if x.is_compatible()]

View file

@ -1,32 +0,0 @@
#!/usr/bin/env python3
# To run use python3 -m pytest --capture=sys
import os
from pokemonterminal.adapter import available_terminals, base
from tests.test_utils import SCRIPT_DIR
def test_available_terminals():
assert available_terminals, 'No available_terminals found.'
terminal_names = [terminal.__name__ for terminal in available_terminals]
non_terminals = ['NullAdapter', '__init__']
assert all(terminal not in terminal_names for terminal in non_terminals)
terminals_dir = os.path.join(SCRIPT_DIR, 'adapter', 'implementations')
assert os.path.isdir(terminals_dir), 'Not found: ' + terminals_dir
for filename in os.listdir(terminals_dir):
terminal, ext = os.path.splitext(filename)
if ext.lower() == '.py':
assert terminal in (terminal_names + non_terminals), terminal
def test_adapter_methods():
for terminal in available_terminals + [base.TerminalAdapterInterface]:
assert callable(terminal.clear)
assert callable(terminal.is_available)
assert callable(terminal.set_image_file_path)
if __name__ == '__main__':
test_available_terminals()

16
tests/test_terminal.py Normal file
View file

@ -0,0 +1,16 @@
from pokemonterminal.terminal import _get_adapter_classes
import os.path as __p
import inspect as __inspct
def test_terminal_adapter_classes():
all_adapter = _get_adapter_classes()
files = {__inspct.getfile(x) for x in all_adapter}
print('all adapter classes:\n', files)
assert len(all_adapter) >= len(files), \
"Some of the files in the adapter module don't define an adapter"
module_name = {x.__name__: __p.splitext(__p.basename(
__inspct.getfile(x)))[0] for x in all_adapter}
print("'class: module' map\n", module_name)
assert all(y.lower() in x.lower() for x, y in module_name.items()), \
"Some of the adapters are defined in unrelated named modules"