diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index a1baae6..513828d 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -5,38 +5,31 @@ jobs: strategy: matrix: os: - - 'ubuntu-latest' + - "ubuntu-latest" runs-on: ${{ matrix.os }} steps: - - name: Setup python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: | - ~/.buildozer - .buildozer - key: ${{ hashFiles('buildozer.spec') }} - - - name: Setup environment - run: | - pip install buildozer - pip install Cython - - run: buildozer --help - - name: SDK, NDK and p4a download - run: | - sed -i.bak "s/# android.accept_sdk_license = False/android.accept_sdk_license = True/" buildozer.spec - buildozer android p4a -- --help - - name: Install Linux dependencies - if: matrix.os == 'ubuntu-latest' - run: sudo apt -y install automake - - name: buildozer android debug - run: | - touch main.py - buildozer android debug - - uses: actions/upload-artifact@v2 - with: - path: bin/*.apk + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: 3.12 + - uses: actions/checkout@v2 + - uses: actions/cache@v4 + with: + path: | + ~/.buildozer + .buildozer + key: ${{ hashFiles('tools/build/buildozer.spec') }} + - name: Setup environment + run: | + pip install buildozer + pip install Cython setuptools + - name: Install Linux dependencies + if: matrix.os == 'ubuntu-latest' + run: sudo apt -y install libltdl-dev automake + - name: buildozer android debug + run: | + sh tools/build/build-android.sh + - uses: actions/upload-artifact@v4.6.1 + with: + path: .buildozer/bin/*.apk diff --git a/.gitignore b/.gitignore index f9a9ead..0843f61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .buildozer +*.pyc +__pycache__/ bin diff --git a/libs/garden/garden.navigationdrawer/LICENSE b/libs/garden/garden.navigationdrawer/LICENSE deleted file mode 100644 index 3094688..0000000 --- a/libs/garden/garden.navigationdrawer/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2013 Alexander Taylor - -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. diff --git a/libs/garden/garden.navigationdrawer/README.md b/libs/garden/garden.navigationdrawer/README.md deleted file mode 100644 index 0ff33d1..0000000 --- a/libs/garden/garden.navigationdrawer/README.md +++ /dev/null @@ -1,128 +0,0 @@ -# NavigationDrawer - -The NavigationDrawer widget provides a hidden panel view designed to -duplicate the popular Android layout. The user views one main widget -but can slide from the left of the screen to view a second, previously -hidden widget. The transition between open/closed is smoothly -animated, with the parameters (anim time, panel width, touch -detection) all user configurable. If the panel is released without -being fully open or closed, it animates to an appropriate -configuration. - -NavigationDrawer supports many different animation properties, -including moving one or both of the side/main panels, darkening -either/both widgets, changing side panel opacity, and changing which -widget is on top. The user can edit these individually to taste (this -is enough rope to hang oneself, it's easy to make a useless or silly -configuration!), or use one of a few preset animations. - -The hidden panel might normally a set of navigation buttons (e.g. in a -GridLayout), but the implementation lets the user use any widget(s). - -The first widget added to the NavigationDrawer is automatically used -as the side panel, and the second widget as the main panel. No further -widgets can be added, further changes are left to the user via editing -the panel widgets. - -# Usage summary - -- The first widget added to a NavigationDrawer is used as the hidden - side panel. -- The second widget added is used as the main panel. -- Both widgets can be removed with remove_widget, or alternatively - set/removed with set_main_panel and set_side_panel. -- The hidden side panel can be revealed by dragging from the left of - the NavigationDrawer. The touch detection width is the - touch_accept_width property. -- Every animation property is user-editable, or default animations - can be chosen by setting anim_type. - -See the example and docstrings for information on individual properties. - - -# Example:: - - from kivy.app import App - from kivy.base import runTouchApp - from kivy.uix.boxlayout import BoxLayout - from kivy.uix.label import Label - from kivy.uix.button import Button - from kivy.uix.image import Image - from kivy.uix.widget import Widget - from kivy.core.window import Window - from kivy.metrics import dp - - from kivy.garden.navigationdrawer import NavigationDrawer - - class ExampleApp(App): - - def build(self): - navigationdrawer = NavigationDrawer() - - side_panel = BoxLayout(orientation='vertical') - side_panel.add_widget(Label(text='Panel label')) - side_panel.add_widget(Button(text='A button')) - side_panel.add_widget(Button(text='Another button')) - navigationdrawer.add_widget(side_panel) - - label_head = ( - '[b]Example label filling main panel[/b]\n\n[color=ff0000](p' - 'ull from left to right!)[/color]\n\nIn this example, the le' - 'ft panel is a simple boxlayout menu, and this main panel is' - ' a BoxLayout with a label and example image.\n\nSeveral pre' - 'set layouts are available (see buttons below), but users ma' - 'y edit every parameter for much more customisation.') - main_panel = BoxLayout(orientation='vertical') - label_bl = BoxLayout(orientation='horizontal') - label = Label(text=label_head, font_size='15sp', - markup=True, valign='top') - label_bl.add_widget(Widget(size_hint_x=None, width=dp(10))) - label_bl.add_widget(label) - label_bl.add_widget(Widget(size_hint_x=None, width=dp(10))) - main_panel.add_widget(Widget(size_hint_y=None, height=dp(10))) - main_panel.add_widget(label_bl) - main_panel.add_widget(Widget(size_hint_y=None, height=dp(10))) - main_panel.add_widget(Image(source='red_pixel.png', allow_stretch=True, - keep_ratio=False, size_hint_y=0.2)) - navigationdrawer.add_widget(main_panel) - label.bind(size=label.setter('text_size')) - - def set_anim_type(name): - navigationdrawer.anim_type = name - modes_layout = BoxLayout(orientation='horizontal') - modes_layout.add_widget(Label(text='preset\nanims:')) - slide_an = Button(text='slide_\nabove_\nanim') - slide_an.bind(on_press=lambda j: set_anim_type('slide_above_anim')) - slide_sim = Button(text='slide_\nabove_\nsimple') - slide_sim.bind(on_press=lambda j: set_anim_type('slide_above_simple')) - fade_in_button = Button(text='fade_in') - fade_in_button.bind(on_press=lambda j: set_anim_type('fade_in')) - reveal_button = Button(text='reveal_\nbelow_\nanim') - reveal_button.bind(on_press= - lambda j: set_anim_type('reveal_below_anim')) - slide_button = Button(text='reveal_\nbelow_\nsimple') - slide_button.bind(on_press= - lambda j: set_anim_type('reveal_below_simple')) - modes_layout.add_widget(slide_an) - modes_layout.add_widget(slide_sim) - modes_layout.add_widget(fade_in_button) - modes_layout.add_widget(reveal_button) - modes_layout.add_widget(slide_button) - main_panel.add_widget(modes_layout) - - button = Button(text='toggle NavigationDrawer state (animate)', - size_hint_y=0.2) - button.bind(on_press=lambda j: navigationdrawer.toggle_state()) - button2 = Button(text='toggle NavigationDrawer state (jump)', - size_hint_y=0.2) - button2.bind(on_press=lambda j: navigationdrawer.toggle_state(False)) - button3 = Button(text='toggle _main_above', size_hint_y=0.2) - button3.bind(on_press=navigationdrawer.toggle_main_above) - main_panel.add_widget(button) - main_panel.add_widget(button2) - main_panel.add_widget(button3) - - return navigationdrawer - - ExampleApp().run() - diff --git a/libs/garden/garden.navigationdrawer/__init__.pyc b/libs/garden/garden.navigationdrawer/__init__.pyc deleted file mode 100644 index 1ba01f1..0000000 Binary files a/libs/garden/garden.navigationdrawer/__init__.pyc and /dev/null differ diff --git a/libs/garden/garden.navigationdrawer/navigationdrawer_closed.png b/libs/garden/garden.navigationdrawer/navigationdrawer_closed.png deleted file mode 100644 index 3b979ae..0000000 Binary files a/libs/garden/garden.navigationdrawer/navigationdrawer_closed.png and /dev/null differ diff --git a/libs/garden/garden.navigationdrawer/navigationdrawer_open_example1.png b/libs/garden/garden.navigationdrawer/navigationdrawer_open_example1.png deleted file mode 100644 index 46b7da6..0000000 Binary files a/libs/garden/garden.navigationdrawer/navigationdrawer_open_example1.png and /dev/null differ diff --git a/libs/garden/garden.navigationdrawer/navigationdrawer_open_example2.png b/libs/garden/garden.navigationdrawer/navigationdrawer_open_example2.png deleted file mode 100644 index 7865607..0000000 Binary files a/libs/garden/garden.navigationdrawer/navigationdrawer_open_example2.png and /dev/null differ diff --git a/background.png b/remoteshell/data/background.png similarity index 100% rename from background.png rename to remoteshell/data/background.png diff --git a/ham_icon.png b/remoteshell/data/ham_icon.png similarity index 100% rename from ham_icon.png rename to remoteshell/data/ham_icon.png diff --git a/icon.png b/remoteshell/data/icon.png similarity index 100% rename from icon.png rename to remoteshell/data/icon.png diff --git a/remoteshell/libs/kivy_console.py b/remoteshell/libs/kivy_console.py new file mode 100644 index 0000000..1506a25 --- /dev/null +++ b/remoteshell/libs/kivy_console.py @@ -0,0 +1,904 @@ +# -*- coding: utf-8 -*- +''' +KivyConsole +=========== + +.. image:: images/KivyConsole.jpg + :align: right + +:class:`KivyConsole` is a :class:`~kivy.uix.widget.Widget` +Purpose: Providing a system console for debugging kivy by running another +instance of kivy in this console and displaying it's output. +To configure, you can use + +cached_history : +cached_commands : +font : +font_size : +shell : + +''Versionadded:: 1.0.?TODO + +''Usage: + from kivy.uix.kivyconsole import KivyConsole + + parent.add_widget(KivyConsole()) + +or + + console = KivyConsole() + +To run a command: + + console.stdin.write('ls -l') + +or + subprocess.Popen(('echo','ls'), stdout = console.stdin) + +To display something on stdout write to stdout + + console.stdout.write('this will be written to the stdout\n') + +or + subprocess.Popen('ps', stdout = console.stdout, shell = True) + +Warning: To read from stdout remember that the process is run in a thread, give +it time to complete otherwise you might get a empty or partial string; +returning whatever has been written to the stdout pipe till the time +read() was called. + + text = console.stdout.read() or read(no_of_bytes) or readline() + +TODO: create a stdin and stdout pipe for + this console like in logger.[==== ]%done +TODO: move everything that is non-specific to + a generic console in a different Project.[ ]%done +TODO: Fix Prompt, make it smaller plus give it more info + +''Shortcuts: +Inside the console you can use the following shortcuts: +Shortcut Function +_________________________________________________________ +PGup Search for previous command inside command history + starting with the text before current cursor position + +PGdn Search for Next command inside command history + starting with the text before current cursor position + +UpArrow Replace command_line with previous command + +DnArrow Replace command_line with next command + (only works if one is not at last command) + +Tab If there is nothing before the cursur when tab is pressed + contents of current directory will be displayed. + '.' before cursur will be converted to './' + '..' to '../' + If there is a path before cursur position + contents of the path will be displayed. + else contents of the path before cursor containing + the commands matching the text before cursur will + be displayed +''' + +__all__ = ('KivyConsole', ) + +import shlex +import subprocess +import re +import os +import sys +from functools import partial +from pygments.lexers import BashSessionLexer + +from kivy.uix.gridlayout import GridLayout +from kivy.properties import (NumericProperty, StringProperty, + BooleanProperty, ObjectProperty, DictProperty, + ListProperty) +from kivy.uix.button import Button +from kivy.uix.textinput import TextInput +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.app import runTouchApp +from kivy.logger import Logger +from kivy.core.window import Window +from kivy.utils import platform + + +Builder.load_string(''' +: + cols:1 + txtinput_history_box: history_box.__self__ + txtinput_command_line: command_line.__self__ + ScrollView: + CodeInput: + id: history_box + size_hint: (1, None) + height: '801dp' + font_name: root.font_name + font_size: root.font_size + readonly: True + foreground_color: root.foreground_color + background_color: root.background_color + on_text: root.on_text(*args) + TextInput: + id: command_line + multiline: False + size_hint: (1, None) + font_name: root.font_name + font_size: root.font_size + readonly: root.readonly + foreground_color: root.foreground_color + background_color: root.background_color + height: '36dp' + on_text_validate: root.on_enter(*args) + on_touch_up: + self.collide_point(*args[1].pos)\\ + and root._move_cursor_to_end(self) +''') + + +class KivyConsole(GridLayout): + '''This is a Console widget used for debugging and running external + commands + + ''' + + readonly = BooleanProperty(False) + '''This defines whether a person can enter commands in the console + + :data:`readonly` is an :class:`~kivy.properties.BooleanProperty`, + Default to 'False' + ''' + + foreground_color = ListProperty((1, 1, 1, 1)) + '''This defines the color of the text in the console + + :data:`foreground_color` is an :class:`~kivy.properties.ListProperty`, + Default to '(1, 1, 1, 1)' + ''' + + background_color = ListProperty((0, 0, 0, 1)) + '''This defines the color of the text in the console + + :data:`foreground_color` is an :class:`~kivy.properties.ListProperty`, + Default to '(0, 0, 0, 1)' + ''' + + cached_history = NumericProperty(200) + '''Indicates the No. of lines to cache. Defaults to 200 + + :data:`cached_history` is an :class:`~kivy.properties.NumericProperty`, + Default to '200' + ''' + + cached_commands = NumericProperty(90) + '''Indicates the no of commands to cache. Defaults to 90 + + :data:`cached_commands` is a :class:`~kivy.properties.NumericProperty`, + Default to '90' + ''' + + font_name = StringProperty('data/fonts/RobotoMono-Regular.ttf') + '''Indicates the font Style used in the console + + :data:`font` is a :class:`~kivy.properties.StringProperty`, + Default to 'Roboto' + ''' + + environment = DictProperty(os.environ.copy()) + '''Indicates the environment the commands are run in. Set your PATH or + other environment variables here. like so:: + + kivy_console.environment['PATH']='path' + + environment is :class:`~kivy.properties.DictProperty`, defaults to + the environment for the pricess running Kivy console + ''' + + font_size = NumericProperty('14sp') + '''Indicates the size of the font used for the console + + :data:`font_size` is a :class:`~kivy.properties.NumericProperty`, + Default to '9' + ''' + + textcache = ListProperty(['', ]) + '''Indicates the cache of the commands and their output + + :data:`textcache` is a :class:`~kivy.properties.ListProperty`, + Default to '' + ''' + + shell = BooleanProperty(False) + '''Indicates the whether system shell is used to run the commands + + :data:`shell` is a :class:`~kivy.properties.BooleanProperty`, + Default to 'False' + + WARNING: Shell = True is a security risk and therefore = False by default, + As a result with shell = False some shell specific commands and + redirections + like 'ls |grep lte' or dir >output.txt will not work. + If for some reason you need to run such commands, try running the platform + shell first + eg: /bin/sh ...etc on nix platforms and cmd.exe on windows. + As the ability to interact with the running command is built in, + you should be able to interact with the native shell. + + Shell = True, should be set only if absolutely necessary. + ''' + + txtinput_command_line = ObjectProperty(None) + + def __init__(self, **kwargs): + self.register_event_type('on_subprocess_done') + self.register_event_type('on_command_list_done') + super(KivyConsole, self).__init__(**kwargs) + # initialisations + self.txtinput_command_line_refocus = False + self.txtinput_run_command_refocus = False + self.win = None + self.scheduled = False + self.command_history = [] + self.command_history_pos = 0 + self.command_status = 'closed' + if sys.version_info >= (3, 0): + self.cur_dir = os.getcwd() + else: + self.cur_dir = os.getcwdu() + self.command_list = [] # list of cmds to be executed + self.stdout = std_in_out(self, 'stdout') + self.stdin = std_in_out(self, 'stdin') + self.popen_obj = None + # self.stderror = stderror(self) + # delayed initialisation + Clock.schedule_once(self._initialize) + self_change_txtcache = self._change_txtcache + _trig = Clock.create_trigger(self_change_txtcache) + self.bind(textcache=_trig) + self._hostname = 'unknown' + try: + if hasattr(os, 'uname'): + self._hostname = os.uname()[1] + else: + self._hostname = os.environ.get('COMPUTERNAME', 'unknown') + except Exception: + pass + self._username = os.environ.get('USER', '') + if not self._username: + self._username = os.environ.get('USERNAME', 'unknown') + + def run_command(self, command, *args): + '''Run a command using Kivy Console. + The output will be visible in the Kivy console. + Returns False if there is a command running and stops. Otherwise + start the execution of the commands and returns True + ''' + if self.popen_obj: + return False + + if isinstance(command, list): + self.command_list = command + else: + self.command_list = [command] + self._run_command_list() + + return True + + def _run_command_list(self, *kwargs): + '''Runs a list of commands + ''' + if self.command_list: + self.stdin.write(self.command_list.pop(0)) + self.bind(on_subprocess_done=self._run_command_list) + else: + self.dispatch('on_command_list_done') + + def clear(self, *args): + '''Clear the Kivy Console area + ''' + self.txtinput_history_box.text = '' + self.textcache = ['', ] + + def _initialize(self, dt): + '''Set console default variable values + ''' + cl = self.txtinput_command_line + self.txtinput_history_box.lexer = BashSessionLexer() + self.txtinput_history_box.text = u''.join(self.textcache) + self.txtinput_command_line.text = self.prompt() + self.txtinput_command_line.bind(focus=self.on_focus) + self.txtinput_command_line.bind( + selection_text=self.on_txtinput_selection) + Clock.schedule_once(self._change_txtcache) + self._focus(self.txtinput_command_line) + self._list = [self.txtinput_command_line] + + def on_txtinput_selection(self, *args): + '''Callback to command input text selection. + Cannot select the PS1 variable, so it'll handle to select only + input text. + ''' + ticl = self.txtinput_command_line + col = len(self.prompt()) + ticl.select_text(col, len(ticl.text)) + + def _move_cursor_to_end(self, instance): + '''Moves the command input cursor to the end + ''' + def mte(*l): + instance.cursor = instance.get_cursor_from_index(len_prompt) + len_prompt = len(self.prompt()) + if instance.cursor[0] < len_prompt: + Clock.schedule_once(mte, -1) + + def _focus(self, widg, t_f=True): + Clock.schedule_once(partial(self._deffered_focus, widg, t_f)) + + def _deffered_focus(self, widg, t_f, dt): + if widg.get_root_window(): + widg.focus = t_f + + def prompt(self, *args): + '''Returns the PS1 variable + ''' + return "[%s@%s %s]>> " % ( + self._username, self._hostname, + os.path.basename(str(self.cur_dir))) + + def _change_txtcache(self, *args): + '''Update the Kivy Console output area + ''' + tihb = self.txtinput_history_box + tihb.text = ''.join(self.textcache) + if not self.get_root_window(): + return + tihb.height = max(tihb.minimum_height, tihb.parent.height) + tihb.parent.scroll_y = 0 + + def on_text(self, instance, txt): + # check if history_box has more text than indicated buy + # self.cached_history and remove excess lines from top + if txt == '': + return + try: + # self._skip_textcache = True + self.textcache = self.textcache[-self.cached_history:] + except IndexError: + pass + # self._skip_textcache = False + + def on_key_down(self, *l): + '''Handle the on_key_down from keyboard + ''' + ticl = self.txtinput_command_line + + def move_cursor_to(col): + '''Update the cursor position + ''' + ticl.cursor =\ + col, ticl.cursor[1] + + def search_history(up_dn): + if up_dn == 'up': + plus_minus = -1 + else: + plus_minus = 1 + l_curdir = len(self.prompt()) + col = ticl.cursor_col + command = ticl.text[l_curdir: col] + max_len = len(self.command_history) - 1 + chp = self.command_history_pos + + while max_len >= 0: + if plus_minus == 1: + if self.command_history_pos > max_len - 1: + self.command_history_pos = max_len + return + else: + if self.command_history_pos <= 0: + self.command_history_pos = max_len + return + self.command_history_pos = self.command_history_pos\ + + plus_minus + cmd = self.command_history[self.command_history_pos] + if cmd[:len(command)] == command: + ticl.text = u''.join(( + self.prompt(), cmd)) + move_cursor_to(col) + return + self.command_history_pos = max_len + 1 + + if ticl.focus: + if l[1] == 273: + # up arrow: display previous command + if self.command_history_pos > 0: + self.command_history_pos = self.command_history_pos - 1 + ticl.text = u''.join( + (self.prompt(), + self.command_history[self.command_history_pos])) + return + if l[1] == 274: + # dn arrow: display next command + if self.command_history_pos < len(self.command_history) - 1: + self.command_history_pos = self.command_history_pos + 1 + ticl.text = u''.join( + (self.prompt(), + self.command_history[self.command_history_pos])) + else: + self.command_history_pos = len(self.command_history) + ticl.text = self.prompt() + col = len(ticl.text) + move_cursor_to(col) + return + if l[1] == 9: + # tab: autocomplete + def display_dir(cur_dir, starts_with=None): + # display contents of dir from cur_dir variable + starts_with_is_not_None = starts_with is not None + try: + dir_list = os.listdir(cur_dir) + except OSError as err: + self.add_to_cache(u''.join((err.strerror, '\n'))) + return + if starts_with_is_not_None: + len_starts_with = len(starts_with) + self.add_to_cache(u''.join(('contents of directory: ', + cur_dir, '\n'))) + txt = u'' + no_of_matches = 0 + for _file in dir_list: + if starts_with_is_not_None: + if _file[:len_starts_with] == starts_with: + # if file matches starts with + txt = u''.join((txt, _file, ' ')) + no_of_matches += 1 + else: + self.add_to_cache(u''.join((_file, '\t'))) + if no_of_matches == 1: + len_txt = len(txt) - 1 + cmdl_text = ticl.text + len_cmdl = len(cmdl_text) + os_sep = os.sep \ + if col == len_cmdl or (col < len_cmdl and + cmdl_text[col] != + os.sep) else '' + ticl.text = u''.join( + (self.prompt(), text_before_cursor, + txt[len_starts_with:len_txt], os_sep, + cmdl_text[col:])) + move_cursor_to(col + (len_txt - len_starts_with) + 1) + elif no_of_matches > 1: + self.add_to_cache(txt) + self.add_to_cache('\n') + + # send back space to command line -remove the tab + Clock.schedule_once(ticl.do_backspace, 0) + l_curdir = len(self.prompt()) + move_cursor_to(l_curdir + len(ticl.text)) + ntext = os.path.expandvars(ticl.text) + # store text before cursor for comparison + col = ticl.cursor_col + if ntext != ticl.text: + ticl.text = ntext + col = len(ntext) + text_before_cursor = ticl.text[l_curdir: col] + + # if empty or space before: list cur dir + if text_before_cursor == ''\ + or ticl.text[col - 1] == ' ': + display_dir(self.cur_dir) + # if in mid command: + else: + # list commands in PATH starting with text before cursor + # split command into path till the seperator + cmd_start = text_before_cursor.rfind(' ') + cmd_start += 1 + cur_dir = self.cur_dir\ + if text_before_cursor[cmd_start] != os.sep\ + else os.sep + os_sep = os.sep if cur_dir != os.sep else '' + cmd_end = text_before_cursor.rfind(os.sep) + len_txt_bef_cur = len(text_before_cursor) - 1 + if cmd_end == len_txt_bef_cur: + # display files in path + if text_before_cursor[cmd_start] == os.sep: + cmd_start += 1 + display_dir(u''.join((cur_dir, os_sep, + text_before_cursor[cmd_start:cmd_end]))) + elif text_before_cursor[len_txt_bef_cur] == '.': + # if / already there return + if len(ticl.text) > col\ + and ticl.text[col] == os.sep: + return + if text_before_cursor[len_txt_bef_cur - 1] == '.': + len_txt_bef_cur -= 1 + if text_before_cursor[len_txt_bef_cur - 1]\ + not in (' ', os.sep): + return + # insert at cursor os.sep: / or \ + ticl.text = u''.join((self.prompt(), + text_before_cursor, os_sep, + ticl.text[col:])) + else: + if cmd_end < 0: + cmd_end = cmd_start + else: + cmd_end += 1 + display_dir(u''.join(( + cur_dir, + os_sep, + text_before_cursor[cmd_start:cmd_end])), + text_before_cursor[cmd_end:]) + return + if l[1] == 280: + # pgup: search last command starting with... + search_history('up') + return + if l[1] == 281: + # pgdn: search next command starting with... + search_history('dn') + return + if l[1] == 278: + # Home: cursor should not go to the left of cur_dir + col = len(self.prompt()) + move_cursor_to(col) + if len(l[4]) > 0 and l[4][0] == 'shift': + ticl.selection_to = col + return + if l[1] == 276 or l[1] == 8: + # left arrow/bkspc: cursor should not go left of cur_dir + col = len(self.prompt()) + if ticl.cursor_col < col: + if l[1] == 8: + ticl.text = self.prompt() + move_cursor_to(col) + return + + def on_focus(self, instance, value): + '''Handle the focus on the command input + ''' + if value: + # focused + if instance is self.txtinput_command_line: + Window.unbind(on_key_down=self.on_key_down) + Window.bind(on_key_down=self.on_key_down) + else: + # defocused + Window.unbind(on_key_down=self.on_key_down) + if self.txtinput_command_line_refocus: + self.txtinput_command_line_refocus = False + if self.txtinput_command_line.get_root_window(): + self.txtinput_command_line.focus = True + self.txtinput_command_line.scroll_x = 0 + if self.txtinput_run_command_refocus: + self.txtinput_run_command_refocus = False + instance.focus = True + instance.scroll_x = 0 + instance.text = u'' + + def add_to_cache(self, _string): + # os.write(self.stdout.stdout_pipe, _string.encode('utf-8')) + # self.stdout.flush() + self.textcache.append(_string) + _string = None + + def kill_process(self, *l): + if self.popen_obj: + self.popen_obj.kill() + + def on_enter(self, *l): + '''When the user press enter and wants to run a command + ''' + self.unbind(on_subprocess_done=self.on_enter) + if self.command_status == 'started': + self.kill_process() + self.bind(on_subprocess_done=self.on_enter) + return + + txtinput_command_line = self.txtinput_command_line + add_to_cache = self.add_to_cache + command_history = self.command_history + + def remove_command_interaction_widgets(*l): + '''command finished:remove widget responsible for interaction + ''' + parent.remove_widget(self.interact_layout) + self.interact_layout = None + # enable running a new command + try: + parent.add_widget(self.txtinput_command_line) + except: + self._initialize(0) + + self._focus(txtinput_command_line, True) + Clock.schedule_once(self._change_txtcache, -1) + self.command_status = 'closed' + self.dispatch('on_subprocess_done') + + def run_cmd(*l): + '''Run the command + ''' + # this is run inside a thread so take care, avoid gui ops + try: + _posix = True + if sys.platform[0] == 'w': + _posix = False + cmd = shlex.split(str(command), posix=_posix)\ + if not self.shell else command + except Exception as err: + cmd = '' + self.add_to_cache(u''.join((str(err), ' <', command, ' >\n'))) + if len(cmd) > 0: + prev_stdout = sys.stdout + sys.stdout = self.stdout + try: + # execute command + self.popen_obj = popen = subprocess.Popen( + cmd, + bufsize=-1, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.STDOUT, + preexec_fn=None, + close_fds=False, + shell=self.shell, + cwd=self.cur_dir, + env=self.environment, + universal_newlines=False, + startupinfo=None, + creationflags=0) + popen_stdout_r = popen.stdout.readline + popen_stdout_flush = popen.stdout.flush + txt = popen_stdout_r() + plat = platform + # print(txt, '<<') + while len(txt) > 0 and self.command_status == 'started': + # skip flush on android + if plat[0] != 'a': + popen_stdout_flush() + add_to_cache(txt.decode('utf8')) + Logger.debug(txt.decode('utf8')) + txt = popen_stdout_r() + except (OSError, ValueError) as err: + add_to_cache(u''.join((str(err.strerror), + ' < ', command, ' >\n'))) + self.command_status = 'closed' + sys.stdout = prev_stdout + self.popen_obj = None + Clock.schedule_once(remove_command_interaction_widgets, 0) + + # append text to textcache + add_to_cache(u''.join((self.txtinput_command_line.text, '\n'))) + command = txtinput_command_line.text[len(self.prompt()):] + + if command == '': + self.txtinput_command_line_refocus = True + return + + # store command in command_history + if self.command_history_pos > 0: + self.command_history_pos = len(command_history) + if command_history[self.command_history_pos - 1] != command: + command_history.append(command) + else: + command_history.append(command) + + len_command_history = len(command_history) + self.command_history_pos = len(command_history) + + # on reaching limit(cached_lines) pop first command + if len_command_history >= self.cached_commands: + self.command_history = command_history[1:] + + # replce $PATH with + command = os.path.expandvars(command) + + # if command = cd change directory + if command == 'clear' or command == 'cls': + self.clear() + txtinput_command_line.text = self.prompt() + self.txtinput_command_line_refocus = True + self.command_status = 'closed' + self.dispatch('on_subprocess_done') + return + if command.startswith('cd ') or command.startswith('export '): + if command[0] == 'e': + e_q = command[7:].find('=') + _exprt = command[7:] + if e_q: + os.environ[_exprt[:e_q]] = _exprt[e_q + 1:] + self.environment = os.environ.copy() + else: + try: + command = re.sub('[ ]+', ' ', command) + if command[3] == os.sep: + os.chdir(command[3:]) + else: + os.chdir(self.cur_dir + os.sep + command[3:]) + if sys.version_info >= (3, 0): + self.cur_dir = os.getcwd() + else: + self.cur_dir = os.getcwdu() + except OSError as err: + Logger.debug('Shell Console: err:' + err.strerror + + ' directory:' + command[3:]) + add_to_cache(u''.join((err.strerror, '\n'))) + txtinput_command_line.text = self.prompt() + self.txtinput_command_line_refocus = True + self.command_status = 'closed' + self.dispatch('on_subprocess_done') + return + + txtinput_command_line.text = self.prompt() + # store output in textcache + parent = txtinput_command_line.parent + # disable running a new command while and old one is running + parent.remove_widget(txtinput_command_line) + # add widget for interaction with the running command + txtinput_run_command = TextInput(multiline=False, + font_size=self.font_size, + font_name=self.font_name) + + def interact_with_command(*l): + '''Text input to interact with the running command + ''' + popen_obj = self.popen_obj + if not popen_obj: + return + txt = l[0].text + u'\n' + popen_obj_stdin = popen_obj.stdin + popen_obj_stdin.write(txt.encode('utf-8')) + popen_obj_stdin.flush() + self.txtinput_run_command_refocus = True + + self.txtinput_run_command_refocus = False + txtinput_run_command.bind(on_text_validate=interact_with_command) + txtinput_run_command.bind(focus=self.on_focus) + btn_kill = Button(text="Stop", + width=60, + size_hint=(None, 1)) + + self.interact_layout = il = GridLayout(rows=1, cols=2, height=27, + size_hint=(1, None)) + btn_kill.bind(on_press=self.kill_process) + il.add_widget(txtinput_run_command) + il.add_widget(btn_kill) + parent.add_widget(il) + + txtinput_run_command.focus = True + self.command_status = 'started' + import threading + threading.Thread(target=run_cmd, daemon=True).start() + # Clock.schedule_once(run_cmd, 0) + + def on_subprocess_done(self, *args): + '''Event handler for when a process was killed + or just finished the execution. + ''' + pass + + def on_command_list_done(self, *args): + '''Event handler for when the whole command list was executed or killed + ''' + pass + + +class std_in_out(object): + ''' class for writing to/reading from this console''' + + def __init__(self, obj, mode='stdout'): + self.obj = obj + self.mode = mode + self.stdin_pipe, self.stdout_pipe = os.pipe() + import threading + threading.Thread(target=self.read_from_in_pipe).start() + self.textcache = None + + def update_cache(self, text_line, obj, *l): + '''Update the output text area + ''' + obj.textcache.append(text_line) + + def read_from_in_pipe(self, *l): + '''Read the output from the command + ''' + txt = '\n' + txt_line = '' + os_read = os.read + self_stdin_pipe = self.stdin_pipe + self_mode = self.mode + self_write = self.write + Clock_schedule_once = Clock.schedule_once + self_update_cache = self.update_cache + self_flush = self.flush + obj = self.obj + try: + while txt != '': + txt = os_read(self_stdin_pipe, 1) + txt_line = u''.join((txt_line, txt)) + if txt == '\n': + if self_mode == 'stdin': + # run command + self_write(txt_line) + else: + Clock_schedule_once( + partial(self_update_cache, txt_line, obj), 0) + self_flush() + txt_line = '' + except OSError as e: + Logger.exception(e) + + def close(self): + '''Close the pipes + ''' + os.close(self.stdin_pipe) + os.close(self.stdout_pipe) + + def __del__(self): + self.close() + + def fileno(self): + return self.stdout_pipe + + def write(self, s): + '''Write a command to the pipe + ''' + Logger.debug('write called with command:' + str(s)) + if self.mode == 'stdout': + self.obj.add_to_cache(s) + self.flush() + else: + # process.stdout.write ...run command + if self.mode == 'stdin': + self.obj.txtinput_command_line.text = ''.join(( + self.obj.prompt(), s)) + self.obj.on_enter() + + def read(self, no_of_bytes=0): + if self.mode == 'stdin': + # stdin.read + Logger.exception('KivyConsole: can not read from a stdin pipe') + return + # process.stdout/in.read + txtc = self.textcache + if no_of_bytes == 0: + # return all data + if txtc is None: + self.flush() + while self.obj.command_status != 'closed': + pass + txtc = self.textcache + return txtc + try: + self.textcache = txtc[no_of_bytes:] + except IndexError: + self.textcache = txtc + return txtc[:no_of_bytes] + + def readline(self): + if self.mode == 'stdin': + # stdin.readline + Logger.exception('KivyConsole: can not read from a stdin pipe') + return + else: + # process.stdout.readline + if self.textcache is None: + self.flush() + txt = self.textcache + x = txt.find('\n') + if x < 0: + Logger.Debug('console_shell: no more data') + return + self.textcache = txt[x:] + # ##self. write to ... + return txt[:x] + + def flush(self): + self.textcache = u''.join(self.obj.textcache) + return + + +if __name__ == '__main__': + runTouchApp(KivyConsole()) diff --git a/remoteshell/libs/kivy_python_console.py b/remoteshell/libs/kivy_python_console.py new file mode 100644 index 0000000..f3326be --- /dev/null +++ b/remoteshell/libs/kivy_python_console.py @@ -0,0 +1,311 @@ +"""TODO: ctrl + left/right (move past word), ctrl + backspace/del (del word), shift + del (del line) + ...: Smart movement through leading indentation. + ...: Except for first line, up/down to work normally on multi-line console input. +""" +from code import InteractiveConsole +from collections import deque +from dataclasses import dataclass +from io import StringIO +from itertools import chain, takewhile +from more_itertools import ilen +import sys +from kivy.uix.codeinput import CodeInput +from pygments.lexers import PythonConsoleLexer + + +@dataclass(frozen=True) +class Key: + # ANY equals everything! -- if you don't care about matching modifiers, set them equal to Key.ANY + ANY = type('ANY', (), { '__eq__': lambda *args: True, + '__repr__': lambda self: 'ANY', + '__hash__': lambda self: -1})() + + code: int + shift: bool = False + ctrl: bool = False + + def __eq__(self, other): + if isinstance(other, int): return other == self.code + return self.__dict__ == other.__dict__ + + def iter_similar(self): + """Return an iterator that yields keys equal to self.""" + yield self + yield Key(self.code, self.shift, Key.ANY) + yield Key(self.code, Key.ANY, self.ctrl) + yield Key(self.code, Key.ANY, Key.ANY) + + +SHIFT, CTRL = (303, 304), (305, 306) + +EXACT = map(Key, (13, 9, 275, 276, 278, 279)) +ANY_MODS = (Key(code, Key.ANY, Key.ANY) for code in (273, 274, 8, 127)) + +KEYS \ + = ENTER, TAB, RIGHT, LEFT, HOME, END, UP, DOWN, BACKSPACE, DELETE \ + = tuple(chain(EXACT, ANY_MODS)) + +del EXACT; del ANY_MODS # Generators exhausted and we don't need them anymore + +CUT = Key(120, False, True) # +COPY = Key(99 , False, True) # +REDO = Key(122, True, True) # + +SELECT_LEFT = Key(276, True, False) # +SELECT_RIGHT = Key(275, True, False) # +SELECT_HOME = Key(278, True, False) # +SELECT_END = Key(279, True, False) # + + +class RedirectConsoleOut: + """Redirect sys.excepthook and sys.stdout in a single context manager. + InteractiveConsole (IC) `write` method won't be used if sys.excepthook isn't sys.__excepthook__, + so we redirect sys.excepthook when pushing to the IC. This redirect probably isn't necessary: + testing was done in IPython which sets sys.excepthook to a crashhandler, but running this file + normally would probably avoid the need for a redirect; still, better safe than sorry. + """ + def __init__(self): + self.stack = deque() + + def __enter__(self): + self.old_hook = sys.excepthook + self.old_out = sys.stdout + + sys.excepthook = sys.__excepthook__ + sys.stdout = StringIO() + + sys.stdout.write('\n') + + def __exit__(self, type, value, tb): + self.stack.append(sys.stdout.getvalue()) + + sys.stdout = self.old_out + sys.excepthook = self.old_hook + + +class Console(InteractiveConsole): + def __init__(self, text_input, locals=None, filename=""): + super().__init__(locals, filename) + self.text_input = text_input + self.out_context = RedirectConsoleOut() + + def push(self, line): + out = self.out_context + with out: needs_more = super().push(line) + + if not needs_more: + out.stack.reverse() + self.text_input.text += ''.join(out.stack) + out.stack.clear() + + return needs_more + + def write(self, data): + self.out_context.stack.append(data) + + +class InputHandler: + def __init__(self, text_input): + self.text_input = text_input + + self.pre = { COPY: self._copy, + CUT: self._cut, + REDO: self._redo} + + self.post = { LEFT: self._left, + RIGHT: self._right, + END: self._end, + HOME: self._home, + SELECT_LEFT: self._select_left, + SELECT_RIGHT: self._select_right, + SELECT_END: self._select_end, + SELECT_HOME: self._select_home, + TAB: self._tab, + ENTER: self._enter, + UP: self._up, + DOWN: self._down, + BACKSPACE: self._backspace} + + def __call__(self, key, read_only): + if handle := self.pre.get(key): return handle + + if read_only: return self._read_only + + for key in key.iter_similar(): + if handle := self.post.get(key): return handle + + def _copy(self, **kwargs): self.text_input.copy() + + def _cut(self, read_only, **kwargs): + self.text_input.copy() if read_only else self.text_input.cut() + + def _redo(self, **kwargs): self.text_input.do_redo() + + def _left(self, at_home, **kwargs): + self.text_input.cancel_selection() + if not at_home: self.text_input.move_cursor('left') + + def _right(self, at_end, **kwargs): + self.text_input.cancel_selection() + if not at_end: self.text_input.move_cursor('right') + + def _end(self, **kwargs): + self.text_input.cancel_selection() + self.text_input.move_cursor('end') + + def _home(self, **kwargs): + self.text_input.cancel_selection() + self.text_input.move_cursor('home') + + def _select_left(self, at_home, has_selection, _from, _to, **kwargs): + if at_home: return + i = self.text_input.move_cursor('left') + if not has_selection: self.text_input.select_text(i, i + 1) + elif i < _from : self.text_input.select_text(i, _to) + elif i >= _from : self.text_input.select_text(_from, i) + + def _select_right(self, at_end, has_selection, _from, _to, **kwargs): + if at_end: return + i = self.text_input.move_cursor('right') + if not has_selection: self.text_input.select_text(i - 1, i) + elif i > _to : self.text_input.select_text(_from, i) + elif i <= _to : self.text_input.select_text(i, _to) + + def _select_end(self, has_selection, _to, _from, i, end, **kwargs): + if not has_selection: start = i + elif _to == i : start = _from + else : start = _to + self.text_input.select_text(start, end) + self.text_input.move_cursor('end') + + def _select_home(self, has_selection, _to, _from, i, home, **kwargs): + if not has_selection: fin = i + elif _from == i : fin = _to + else : fin = _from + self.text_input.select_text(home, fin) + self.text_input.move_cursor('home') + + def _tab(self, has_selection, at_home, **kwargs): + ti = self.text_input + if not has_selection and at_home: ti.insert_text(' ' * ti.tab_width) + + def _enter(self, home, **kwargs): + ti = self.text_input + text = ti.text[home:].rstrip() + + if text and (len(ti.history) == 1 or ti.history[1] != text): + ti.history.popleft() + ti.history.appendleft(text) + ti.history.appendleft('') + ti._history_index = 0 + + needs_more = ti.console.push(text) + ti.prompt(needs_more) + + def _up(self, **kwargs): self.text_input.input_from_history() + + def _down(self, **kwargs): self.text_input.input_from_history(reverse=True) + + def _backspace(self, at_home, has_selection, window, keycode, text, modifiers, **kwargs): + ti = self.text_input + if not at_home or has_selection: + super(KivyConsole, ti).keyboard_on_key_down(window, keycode, text, modifiers) + + def _read_only(self, key, window, keycode, text, modifiers, **kwargs): + ti = self.text_input + ti.cancel_selection() + ti.move_cursor('end') + if key.code not in KEYS: + super(KivyConsole, ti).keyboard_on_key_down(window, keycode, text, modifiers) + + +class KivyConsole(CodeInput): + prompt_1 = '\n>>> ' + prompt_2 = '\n... ' + + _home_pos = 0 + _indent_level = 0 + _history_index = 0 + + def __init__(self, *args, locals=None, banner=None, **kwargs): + super().__init__(*args, **kwargs) + self.lexer = PythonConsoleLexer() + self.history = deque(['']) + self.console = Console(self, locals) + self.input_handler = InputHandler(self) + + if banner is None: + self.text = (f'Python {sys.version.splitlines()[0]}\n' + 'Welcome to the KivyConsole -- A Python interpreter widget for Kivy!\n') + else: self.text = banner + self.prompt() + + def prompt(self, needs_more=False): + if needs_more: + prompt = self.prompt_2 + self._indent_level = self.count_indents() + if self.text.rstrip().endswith(':'): self._indent_level += 1 + else: + prompt = self.prompt_1 + self._indent_level = 0 + + indent = self.tab_width * self._indent_level + self.text += prompt + ' ' * indent + self._home_pos = self.cursor_index() - indent + self.reset_undo() + + def count_indents(self): + return ilen(takewhile(str.isspace, self.history[1])) // self.tab_width + + def keyboard_on_key_down(self, window, keycode, text, modifiers): + """Emulate a python console: disallow editing of previous console output.""" + if keycode[0] in CTRL or keycode[0] in SHIFT and 'ctrl' in modifiers: return + + key = Key(keycode[0], 'shift' in modifiers, 'ctrl' in modifiers) + + # force `selection_from` <= `selection_to` (mouse selections can reverse the order): + _from, _to = sorted((self.selection_from, self.selection_to)) + has_selection = bool(self.selection_text) + i, home, end = self.cursor_index(), self._home_pos, len(self.text) + + read_only = i < home or has_selection and _from < home + at_home = i == home + at_end = i == end + + kwargs = locals(); del kwargs['self'] + if handle := self.input_handler(key, read_only): return handle(**kwargs) + + return super().keyboard_on_key_down(window, keycode, text, modifiers) + + def move_cursor(self, pos): + """Similar to `do_cursor_movement` but we account for `_home_pos` and we return the new cursor index.""" + if pos == 'end' : index = len(self.text) + elif pos == 'home' : index = self._home_pos + elif pos == 'left' : index = self.cursor_index() - 1 + elif pos == 'right': index = self.cursor_index() + 1 + self.cursor = self.get_cursor_from_index(index) + return index + + def input_from_history(self, reverse=False): + self._history_index += -1 if reverse else 1 + self._history_index = min(max(0, self._history_index), len(self.history) - 1) + self.text = self.text[: self._home_pos] + self.history[self._history_index] + + +if __name__ == "__main__": + from textwrap import dedent + from kivy.app import App + from kivy.lang import Builder + + KV = """ + KivyConsole: + font_name : './UbuntuMono-R.ttf' + style_name: 'monokai' + """ + + + class KivyInterpreter(App): + def build(self): return Builder.load_string(dedent(KV)) + + + KivyInterpreter().run() \ No newline at end of file diff --git a/libs/garden/garden.navigationdrawer/__init__.py b/remoteshell/libs/navigationdrawer/__init__.py similarity index 99% rename from libs/garden/garden.navigationdrawer/__init__.py rename to remoteshell/libs/navigationdrawer/__init__.py index 7021e67..f899d0b 100644 --- a/libs/garden/garden.navigationdrawer/__init__.py +++ b/remoteshell/libs/navigationdrawer/__init__.py @@ -445,7 +445,7 @@ def set_main_panel(self, widget): # Clear existing side panel entries if len(self._main_panel.children) > 0: for child in self._main_panel.children: - self._main_panel.remove(child) + self._main_panel.remove_widget(child) # Set new side panel self._main_panel.add_widget(widget) self.main_panel = widget diff --git a/remoteshell/libs/navigationdrawer/_version.py b/remoteshell/libs/navigationdrawer/_version.py new file mode 100644 index 0000000..a6221b3 --- /dev/null +++ b/remoteshell/libs/navigationdrawer/_version.py @@ -0,0 +1 @@ +__version__ = '1.0.2' diff --git a/libs/garden/garden.navigationdrawer/navigationdrawer_gradient_ltor.png b/remoteshell/libs/navigationdrawer/navigationdrawer_gradient_ltor.png similarity index 100% rename from libs/garden/garden.navigationdrawer/navigationdrawer_gradient_ltor.png rename to remoteshell/libs/navigationdrawer/navigationdrawer_gradient_ltor.png diff --git a/libs/garden/garden.navigationdrawer/navigationdrawer_gradient_rtol.png b/remoteshell/libs/navigationdrawer/navigationdrawer_gradient_rtol.png similarity index 100% rename from libs/garden/garden.navigationdrawer/navigationdrawer_gradient_rtol.png rename to remoteshell/libs/navigationdrawer/navigationdrawer_gradient_rtol.png diff --git a/remoteshell/libs/navigationdrawer/tests/test_import.py b/remoteshell/libs/navigationdrawer/tests/test_import.py new file mode 100644 index 0000000..9bbe60d --- /dev/null +++ b/remoteshell/libs/navigationdrawer/tests/test_import.py @@ -0,0 +1,6 @@ +import pytest + + +def test_flower(): + from kivy_garden.navigationdrawer import NavigationDrawer + widget = NavigationDrawer() diff --git a/main.py b/remoteshell/main.py similarity index 95% rename from main.py rename to remoteshell/main.py index b0a682b..392a574 100644 --- a/main.py +++ b/remoteshell/main.py @@ -16,8 +16,10 @@ from kivy.properties import StringProperty from kivy.app import App -from kivy.garden import navigationdrawer +from libs.navigationdrawer import NavigationDrawer from kivy.uix.screenmanager import Screen +from kivy.core.window import Window +Window.softinput_mode = 'below_target' app = None #+---[RSA 3072]----+ @@ -120,7 +122,10 @@ class MainScreen(Screen): def __init__(self, **kwargs): super(MainScreen, self).__init__(**kwargs) - ip = socket.gethostbyname(socket.gethostname()) + try: + ip = socket.gethostbyname(socket.gethostname()) + except Exception as Error: + ip = socket.gethostbyname('localhost') if ip.startswith('127.'): interfaces = ['eth0', 'eth1', 'eth2', 'wlan0', 'wlan1', 'wifi0', 'tiwlan0', 'tiwlan1', 'ath0', 'ath1', 'ppp0'] diff --git a/plyer_command_list.rst b/remoteshell/plyer_command_list.rst similarity index 81% rename from plyer_command_list.rst rename to remoteshell/plyer_command_list.rst index d9f4d83..3efa784 100644 --- a/plyer_command_list.rst +++ b/remoteshell/plyer_command_list.rst @@ -17,7 +17,7 @@ Command list - ACCELERATOR_ - GYROSCOPE_ - CALL_ - + TTS(Text to speech) ------------------- @@ -28,33 +28,6 @@ Example:: top_ -GPS ---- - -.. _GPS: - -.. note:: - - This will work only on versions before android 6.0 . - - For android 6.0 + the coder needs to explictly ask permissions. - - -Here is an example of the usage of gps:: - - from plyer import gps - coordinate = 0 - def print_locations(**kwargs): - global coordinate - coordinate = kwargs - - gps.configure(on_location=print_locations) - gps.start() - # later - print coordinate - gps.stop() - - Notification ------------ @@ -207,4 +180,3 @@ IrBlaster Example:: from plyer import irblaster - diff --git a/remotekivy.kv b/remoteshell/remotekivy.kv similarity index 56% rename from remotekivy.kv rename to remoteshell/remotekivy.kv index e301b66..eab17ce 100644 --- a/remotekivy.kv +++ b/remoteshell/remotekivy.kv @@ -11,9 +11,13 @@ text: open('plyer_command_list.rst').read() + #KivyConsole + opacity: 1 on_release: app.root.ids.manager.current = self.text.lower() + size_hint_y: None + height: sp(64) orientation: 'vertical' @@ -23,9 +27,13 @@ text: 'Plyer' NavigationButton text: 'Console' + NavigationButton + text: 'Python Interpretter' + Widget - size_hint_y: .2 + size_hint_y: None + height: sp(64) pos_hint: {'top': 1} canvas: Color: @@ -37,13 +45,13 @@ size_hint_x: None width: self.height border: 0, 0, 0, 0 - background_normal: 'ham_icon.png' - background_down: 'ham_icon.png' + background_normal: 'data/ham_icon.png' + background_down: 'data/ham_icon.png' opacity: 1 if self.state == 'normal' else .5 on_state: app.root.toggle_state() Widget Image: - source: 'icon.png' + source: 'data/icon.png' mipmap: True size_hint_x: None width: '100dp' @@ -59,7 +67,7 @@ NavigationDrawer NavigationScreen Screen Image: - source: 'background.png' + source: 'data/background.png' allow_stretch: True keep_ratio: False BoxLayout @@ -69,8 +77,22 @@ NavigationDrawer id: manager MainScreen name: 'remote' - # CommandScreen - # name: 'plyer' + CommandScreen + name: 'plyer' ShellScreen name: 'console' - + on_enter: + if not self.children:\ + from libs.kivy_console import KivyConsole as ShellConsole;\ + sc = ShellConsole();\ + sc.font_name = 'RobotoMono-Regular.ttf';\ + self.add_widget(sc) + Screen + name: 'python interpretter' + on_enter: + if not self.children:\ + from libs.kivy_python_console import KivyConsole as PythonConsole;\ + pc = PythonConsole();\ + pc.font_name = 'RobotoMono-Regular.ttf';\ + pc.style_name = 'monokai';\ + self.add_widget(pc) diff --git a/test.py b/tests/test.py similarity index 100% rename from test.py rename to tests/test.py diff --git a/tools/build/build-android.sh b/tools/build/build-android.sh new file mode 100644 index 0000000..79acc90 --- /dev/null +++ b/tools/build/build-android.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -eux pipefail + +ROOT=$(realpath $(dirname "$0")/../..) + +cd "$ROOT/tools/build" + +BUILDOZER_BIN_DIR="$ROOT/.buildozer/bin" BUILDOZER_BUILD_DIR="${ROOT}"/.buildozer buildozer android debug deploy run logcat diff --git a/buildozer.spec b/tools/build/buildozer.spec similarity index 72% rename from buildozer.spec rename to tools/build/buildozer.spec index 3c76fc0..f616cdd 100644 --- a/buildozer.spec +++ b/tools/build/buildozer.spec @@ -1,5 +1,6 @@ [app] +icon=%(source.dir)s/data/icon.png # title of the application title = Kivy Remote Shell @@ -10,7 +11,7 @@ package.name = remoteshell package.domain = org.kivy # indicate where the source code is living -source.dir = . +source.dir = ../../remoteshell source.include_exts = py,png,kv,rst # search the version information into the source code @@ -18,7 +19,7 @@ version.regex = __version__ = '(.*)' version.filename = %(source.dir)s/main.py # requirements of the app -requirements = android,cryptography,pyasn1,bcrypt,attrs,twisted,kivy,docutils,pygments,cffi +requirements = android,cryptography,pyasn1,bcrypt,attrs,twisted,kivy,docutils,pygments,cffi, more_itertools,plyer # android specific android.permissions = INTERNET, WAKE_LOCK, CAMERA, VIBRATE, ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION, SEND_SMS, CALL_PRIVILEGED, CALL_PHONE, BLUETOOTH @@ -27,13 +28,20 @@ android.permissions = INTERNET, WAKE_LOCK, CAMERA, VIBRATE, ACCESS_COARSE_LOCATI #android.api=22 android.accept_sdk_license=True + +# (str) Android logcat filters to use +android.logcat_filters = *:S python,mediaserver,SDL:D android.wakelock=True + orientation=portrait fullscreen=True p4a.branch = develop +p4a.local_recipes = p4a_recipes -#presplash.filename= +#presplash.filename= [buildozer] log_level = 2 warn_on_root = 1 +#bin_dir = ../..//buildozer/bin +#build_dir = ../../.buildozer diff --git a/tools/build/p4a_recipes/cryptography/__init__.py b/tools/build/p4a_recipes/cryptography/__init__.py new file mode 100644 index 0000000..182c745 --- /dev/null +++ b/tools/build/p4a_recipes/cryptography/__init__.py @@ -0,0 +1,22 @@ +from pythonforandroid.recipe import CompiledComponentsPythonRecipe, Recipe + + +class CryptographyRecipe(CompiledComponentsPythonRecipe): + name = 'cryptography' + version = '2.8' + url = 'https://github.com/pyca/cryptography/archive/{version}.tar.gz' + depends = ['openssl', 'six', 'setuptools', 'cffi'] + call_hostpython_via_targetpython = False + + def get_recipe_env(self, arch): + env = super().get_recipe_env(arch) + + openssl_recipe = Recipe.get_recipe('openssl', self.ctx) + env['CFLAGS'] += openssl_recipe.include_flags(arch) + env['LDFLAGS'] += openssl_recipe.link_dirs_flags(arch) + env['LIBS'] = openssl_recipe.link_libs_flags() + + return env + + +recipe = CryptographyRecipe() diff --git a/requirements.txt b/tools/build/requirements.txt similarity index 79% rename from requirements.txt rename to tools/build/requirements.txt index af72371..47c63b7 100644 --- a/requirements.txt +++ b/tools/build/requirements.txt @@ -6,3 +6,5 @@ pycrypto==2.6.1 requests==2.32.2 Twisted==24.7.0 #zope.interface==4.3.2 +more_itertools==2.2 +plyer==master