Craig Swank avatar Craig Swank committed 692fd5f Merge

Merge branch 'master' of bitbucket.org:cswank/gadgets

Comments (0)

Files changed (31)

+
+CHANGES
+
+
+0.3.1, 7/25/2013 -
+    added a request-response mechanism so the status of the system
+    can be requested.
+    Added a reference to the iphone interface to the docs.
 0.3.0, 7/18/2013 -
     improved the 'command mode' of the ui
     improved the note taking dialog of the ui

docs/source/ui/command_mode.rst

 User Interface Command Mode
 ============================
 
-Let's turn on something.  As the help menu shows, hitting 'c'  enters
-you into command mode.  Hit any key to exit the help menu and then
-hit 'c'.  The bar at the top shows you are now in 'command mode':
-
-.. image:: images/ui_3.png
-
-Command mode is kind of strange, since I'm no User Interface master.  Here is how it
-works.  While you are in command mode, none of the key bindings that the help menu
-showed you will work.  Instead the keys you press are used to navigate the command mode.
-For example, to turn on the hlt heater you press h (for hlt) then h (for heater).  Now
-the screen looks like this (the effect is pretty subtle on the terminal I'm using here,
-I need to improve that):
+Let's turn on something.  Type 'c' to enter command mode, which should
+now be indicated by the top bar.  
 
 .. image:: images/ui_4.png
 
-You can see that 'hlt' and 'heater' are bold.  Now hit the enter key to turn on the heater:
+Once the ui is in command mode you can use the up and down arrows (or
+for emacs folks like myself, the 'n' and 'p' keys) to highlight the
+output devices in the device tree.
 
 .. image:: images/ui_5.png
 
-To turn it off hit enter again.
+Once the output device you wish to control is highlighed, hit the enter
+key to turn it on.
 
-To turn on some other device you have to first hit 'esc' to exit command mode, then enter
-command mode again by hitting 'c'.
+.. image:: images/ui_6.png
 
-You can also send a command with arguments.  For instance, if you wanted turn on the fan
-for 10 minutes you would hit the following keys 'c' -> 'br' -> 'f' (note you have to type
-'br' because there are two locations, brewery and boiler, that both start with 'b').  Now
-the brewery fan should be bold.  Instead of hitting 'c', you hit 'a' (for arguments).
-You will be prompted for the arguments by a pop up window:
+To turn it off hit enter again.
 
-.. image:: images/ui_6.png
+You can also send a command with arguments.  For instance, if you wanted
+turn on the pump for 5 seconds you can highlight the target device and
+hit the 'a' key (a for arguments).  You will be prompted for the
+arguments by a pop up window:
+
+.. image:: images/ui_7.png
 
-Hit enter after typing the arguments and the fan will turn on for 5 seconds, then turn off 
-(see the docs for Robot Command Language for an explaination on command arguments).
+Hit enter after typing the arguments and the bet two pump will turn on
+for 5 seconds, then turn off  (see the docs for Robot Command Language
+for an explaination on command arguments).
 
-Once again, hit 'esc' to exit command mode.
+Hit 'esc' to exit command mode.
Add a comment to this file

docs/source/ui/images/iphone.png

Added
New image
Add a comment to this file

docs/source/ui/images/ui_4.png

Old
Old image
New
New image
Add a comment to this file

docs/source/ui/images/ui_5.png

Old
Old image
New
New image
Add a comment to this file

docs/source/ui/images/ui_6.png

Old
Old image
New
New image
Add a comment to this file

docs/source/ui/images/ui_7.png

Old
Old image
New
New image

docs/source/ui/ui.rst

    help
    command_mode
    method
+
+You can also download the source code for an `iPhone interface <https://bitbucket.org/cswank/iphonegadgets/>`_ and
+install it on your phone (if you are a registered Apple Developer).  I'm 
+working on getting the iPhone interace released on the app store.
+
+.. image:: images/iphone.png

examples/sprinklers.py

             'sprinklers': {
                 'type': 'switch',
                 'pin': pins['gpio'][11],
-                'on': 'water {location}'
+                'on': 'water {location}',
                 'off': 'stop watering {location}'
                 },
             },
             'sprinklers': {
                 'type': 'switch',
                 'pin': pins['gpio'][13],
-                'on': 'water {location}'
+                'on': 'water {location}',
                 'off': 'stop watering {location}'
                 },
             },
             'sprinklers': {
                 'type': 'switch',
                 'pin': pins['gpio'][15],
-                'on': 'water {location}'
+                'on': 'water {location}',
                 'off': 'stop watering {location}'
                 },
             },
             'sprinklers': {
                 'type': 'switch',
                 'pin': pins['gpio'][16],
-                'on': 'water {location}'
+                'on': 'water {location}',
                 'off': 'stop watering {location}'
                 },
             },
             'sprinklers': {
                 'type': 'switch',
                 'pin': pins['gpio'][18],
-                'on': 'water {location}'
+                'on': 'water {location}',
                 'off': 'stop watering {location}'
                 },
             },
 accepts a '--command' argument, which starts a Socket, sends the command, and
 then exits.
 
-# Minute   Hour   Day of Month       Month          Day of Week         Command
-# (0-59)  (0-23)     (1-31)    (1-12 or Jan-Dec)  (0-6 or Sun-Sat) 
-    0       5          *             *                Wed               gadgets --command 'water front yard for 20 minutes'
-    0       5          *             *                Fri               gadgets --command 'water front yard for 20 minutes'
-    25      5          *             *                Wed               gadgets --command 'water back yard for 20 minutes'
-    25      5          *             *                Fri               gadgets --command 'water back yard for 20 minutes'
-    50      5          *             *                Wed               gadgets --command 'water sidewalk for 20 minutes'
-    50      5          *             *                Fri               gadgets --command 'water sidewalk for 20 minutes'
-    0       4          *             *                Mon               gadgets --command 'water front garden for 5 minutes'
-    0       4          *             *                Tue               gadgets --command 'water front garden for 5 minutes'
-    0       4          *             *                Wed               gadgets --command 'water front garden for 5 minutes'    
-    0       4          *             *                Thu               gadgets --command 'water front garden for 5 minutes'
-    0       4          *             *                Fri               gadgets --command 'water front garden for 5 minutes'
-    0       4          *             *                Sat               gadgets --command 'water front garden for 5 minutes'
-    0       4          *             *                Sun               gadgets --command 'water front garden for 5 minutes'
-    0       4          *             *                Mon               gadgets --command 'water back garden for 5 minutes'
-    0       4          *             *                Tue               gadgets --command 'water back garden for 5 minutes'
-    0       4          *             *                Wed               gadgets --command 'water back garden for 5 minutes'    
-    0       4          *             *                Thu               gadgets --command 'water back garden for 5 minutes'
-    0       4          *             *                Fri               gadgets --command 'water back garden for 5 minutes'
-    0       4          *             *                Sat               gadgets --command 'water back garden for 5 minutes'
-    0       4          *             *                Sun               gadgets --command 'water back garden for 5 minutes'
+# Minute   Hour   Day of Month       Month          Day of Week        Command    
+# (0-59)  (0-23)     (1-31)    (1-12 or Jan-Dec)  (0-6 or Sun-Sat)
+0   5   *  *  Wed  gadgets --command 'water front yard for 20 minutes'
+21  5   *  *  Wed  gadgets --command 'water back yard for 20 minutes'
+0   5   *  *  Sat  gadgets --command 'water front yard for 20 minutes'
+21  5   *  *  Sat  gadgets --command 'water back yard for 20 minutes'
+0   4   *  *  *    gadgets --command 'water back garden for 5 minutes'
+42  9-18 * * * gadgets --command 'water front garden for 2 minutes'
+50  9-18 * * * gadgets --command 'water sidewalk for 2 minutes'
+0   3 * * * python2 gadgets --command 'water sidewalk for 30 minutes'
+
+NOTE:  I just planted a bunch of plants in the sidewalk zone and the front garden and it is the middle of summer.  I
+am watering them for 2 minutes each hour during the day so they don't dry out, not because I'm a water wasting bastard.
 """

gadgets/coordinator.py

     @property
     def sockets(self):
         if self._sockets is None:
-            self._sockets = Sockets(self._addresses, events=self._events)
+            self._sockets = Sockets(self._addresses, events=self._events, bind_to_request=True)
         return self._sockets
 
     def _recv(self):
-        event, message = self.sockets.recv()
+        event, message, socket = self.sockets.recv_all()
         return_value = False
-        if event in self._event_handlers:
-            f = self._event_handlers[event]
-            return_value = f(message)
+        if socket == 'subscriber':
+            if event in self._event_handlers:
+                f = self._event_handlers[event]
+                return_value = f(message)
+        elif socket == 'request': #as of now, request is only for getting the status of the system
+            self._handle_request(event, message)
+            
         return return_value
 
+    def _handle_request(self, event, message):
+        if event == 'status':
+            response = self._state
+        elif event == 'events':
+            response = self._external_events
+        else:
+            response = {'error': 'you can only request "events", or "status"'}
+        self.sockets.respond(event, response)
+
     def _run_method(self, message):
         """
         a method message looks like:

gadgets/sockets.py

     as one system.
     """
 
-    def __init__(self, host='localhost', in_port=6111, out_port=6112):
+    def __init__(self, host='localhost', in_port=6111, out_port=6112, req_port=6113):
         """
         host: the host name of the master gadgets instance
               (the one that is running with localhost as the
         self.in_bind_address = 'tcp://*:{0}'.format(out_port)
         self.in_address = 'tcp://{0}:{1}'.format(host, in_port)
         self.out_bind_address = 'tcp://*:{0}'.format(in_port)
+        self.req_address = 'tcp://{0}:{1}'.format(host, req_port)
+        self.req_bind_address = 'tcp://*:{0}'.format(req_port)
 
         
 class Broker(threading.Thread):
         """
         addresses: an instance of gadgets.address:Address
         """
+        
         if addresses is None:
             self._addresses = Addresses()
         else:
     what events will be received.
     """
     
-    def __init__(self, addresses=None, events=[]):
+    def __init__(self, addresses=None, events=[], bind_to_request=False):
         """
         addresses: an instance of gadgets.address:Address
         events: a list of events to subscribe to.  Only 
                       you will receive messages like
                       'update temperature'
         """
+        self.bind_to_request = bind_to_request
+        self._poller = None
         self.context = zmq.Context.instance()
         if addresses is None:
             addresses = Addresses()
             self.subscriber.setsockopt(zmq.SUBSCRIBE, event)
         self.publisher = self.context.socket(zmq.PUB)
         self.publisher.connect(addresses.out_address)
+        
+        if bind_to_request:
+            self.req = self.context.socket(zmq.REP)
+            self.req.bind(addresses.req_bind_address)
+        else:
+            self.req = self.context.socket(zmq.REQ)
+            self.req.connect(addresses.req_address)
         time.sleep(0.2)
         
     def send(self, event, message={}):
             raise Exception(str(parts))
         event, message = parts
         return event, json.loads(message)
+    
+    def request(self, event, message={}):
+        self.req.send_multipart([event, json.dumps(message, ensure_ascii=True)])
+        parts = self.req.recv_multipart()
+        if len(parts) != 2:
+            raise Exception(str(parts))
+        event, message = parts
+        return event, json.loads(message)
+
+    def respond(self, event, message={}):
+        self.req.send_multipart([event, json.dumps(message, ensure_ascii=True)])
+
+    def recv_all(self):
+        socks = dict(self.poller.poll())
+        if self.subscriber in socks and socks[self.subscriber] == zmq.POLLIN:
+            event, message = self.recv()
+            return event, message, 'subscriber'
+        if self.req in socks and socks[self.req] == zmq.POLLIN:
+            parts = self.req.recv_multipart()
+            if len(parts) != 2:
+                raise Exception(str(parts))
+            event, message = parts
+            return event, message, 'request'
+        return None, None, None
+
+    @property
+    def poller(self):
+        if self._poller is None:
+            poller = zmq.Poller()
+            poller.register(self.subscriber, zmq.POLLIN)
+            poller.register(self.req, zmq.POLLIN)
+            self._poller = poller
+        return self._poller
         
+
     def close(self):
         self.publisher.close()
         self.subscriber.close()
+        self.req.close()
 

gadgets/tests/test_cooler.py

 
 
 path = '/tmp/cooler'
-port = random.randint(3000, 50000)
+
+port = 0
 
 
 class FakeGPIO(object):
 
     def __init__(self):
-        self.addresses = Addresses(in_port=port, out_port=port+1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.sockets = Sockets(self.addresses)
         self.status = False
 
 class TestCooler(object):
 
     def setup(self):
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        global port
+        port = random.randint(3000, 50000)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.sockets = Sockets(self.addresses, events=['test update'])
         self.cooler = cooler_factory('tank', 'cooler', {}, self.addresses, io_factory=get_fake_gpio)
         self.gadgets = Gadgets([self.cooler], self.addresses)
 
     def teardown(self):
+        print 'sending shutdown'
         self.sockets.send('shutdown')
         time.sleep(0.2)
         self.sockets.close()

gadgets/tests/test_coordinator.py

 
     def setup(self):
         port = random.randint(5000, 50000)
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.coordinator = Coordinator(self.addresses, 'testsys')
 
     def test_create(self):
         time.sleep(0.2)
         eq_(self.coordinator._ids, ['garage opener'])
         self.coordinator.sockets.send('shutdown')
+
+    def test_request_socket(self):
+        sockets = Sockets(self.addresses)
+        broker = Broker(self.addresses)
+        broker.start()
+        time.sleep(0.5)
+        self.coordinator.start()
+        time.sleep(1)
         
+        status = sockets.request('status')
+        eq_(status, ('status', {u'locations': {}, u'errors': [], u'method': {}, u'name': u'testsys'}))
+        sockets.send('shutdown')
+
+        while self.coordinator.is_alive():
+            time.sleep(0.1)
+        sockets.close()
+
+    def test_request_socket_events(self):
+        sockets = Sockets(self.addresses)
+        broker = Broker(self.addresses)
+        broker.start()
+        time.sleep(0.5)
+        self.coordinator.start()
+        time.sleep(1)
+        
+        status = sockets.request('events')
+        eq_(status, ('events', {}))
+        sockets.send('shutdown')
+
+        while self.coordinator.is_alive():
+            time.sleep(0.1)
+        sockets.close()
     

gadgets/tests/test_device.py

 
     def setup(self):
         port = random.randint(5000, 50000)
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.sockets = Sockets(self.addresses, events=['update'])
         self.device = Device(
             'living room',

gadgets/tests/test_float_trigger.py

 import time
+import random
 from gadgets.devices.valve.triggers import FloatTrigger
 from gadgets import Addresses
 
 class TestFloatTrigger(object):
 
     def setup(self):
-        self.addresses = Addresses()
+        port = random.randint(5000, 50000)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         FloatTrigger._poller_class = FakePoller
         self.trigger = FloatTrigger(
             'tank',

gadgets/tests/test_gadgets.py

-import time, threading, random, uuid
+import time, threading, random, uuid, platform
 from nose.tools import eq_, raises
 from gadgets import Gadgets, Addresses, Sockets, Broker
 from gadgets.devices.device import Device
 
     def setup(self):
         port = random.randint(5000, 50000)
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.gadgets = Gadgets([], self.addresses)
 
     def test_create(self):
         event, message = sockets.recv()
         sockets.send('shutdown')
         eq_(event, uid + ' status')
-        eq_(message, {u'name': u'pjoe', u'errors': [], u'locations': {u'back yard': {u'sprinklers': {u'value': True}}}, u'method': {}})
+        name = platform.node()
+        eq_(message, {u'name': name, u'errors': [], u'locations': {u'back yard': {u'sprinklers': {u'value': True}}}, u'method': {}})
 

gadgets/tests/test_gadgets_factory.py

-import threading, time
+import threading, time, random
 from gadgets import Addresses, GadgetsFactory, Sockets
 from gadgets.devices import Switch, Valve
 from gadgets.pins.beaglebone import pins
 class TestGadgetsFactory(object):
 
     def setup(self):
-        self.addresses = Addresses()
+        port = random.randint(5000, 50000)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.factory = GadgetsFactory(self.addresses)
 
     def test_create(self):
         assert isinstance(devices[0], Switch)
 
     def test_create_float_valve(self):
+        sockets = Sockets(self.addresses)
         args = {
             'locations': {
                 'fish tank': {
         t.start()
         time.sleep(1)
         eq_(dict(gadgets.coordinator._external_events), {u'fish tank': {u'valve': {'on': u'fill fish tank', 'off': u'stop filling fish tank'}}})
-        sockets = Sockets()
         sockets.send('shutdown')
         assert isinstance(devices[0], Valve)
+        while gadgets.coordinator.is_alive():
+            time.sleep(0.1)
         sockets.close()

gadgets/tests/test_gravity_trigger.py

     def setup(self):
         self._off = False
         port = random.randint(5000, 50000)
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.sockets = Sockets(self.addresses)
         self.gadgets = Gadgets([], self.addresses)
         

gadgets/tests/test_heater.py

 class FakeGPIO(object):
 
     def __init__(self):
-        self.addresses = Addresses(in_port=port, out_port=port+1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.sockets = Sockets(self.addresses)
         self.status = False
         self.closed = False
 class TestHeater(object):
 
     def setup(self):
-        self.addresses = Addresses(in_port=port, out_port=port+1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.sockets = Sockets(self.addresses, events=['test pwm off'])
         
         self.heater = ElectricHeater(

gadgets/tests/test_input.py

 
     def setup(self):
         port = random.randint(5000, 50000)
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         pin = None
         self.sockets = Sockets(self.addresses, events=['update'])
         self.input = input_factory('left', 'input', {'pin':None}, self.addresses)

gadgets/tests/test_input_adc.py

 
     def setup(self):
         port = random.randint(5000, 50000)
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         pin = None
         self.sockets = Sockets(self.addresses, events=['update'])
         self.input = input_factory('left', 'input', {'pin':None, 'input_type': 'adc'}, self.addresses)

gadgets/tests/test_method.py

 
     def setup(self):
         port = random.randint(5000, 50000)
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.sockets = Sockets(self.addresses, events=['turn', 'heat', 'drain'])
         self.gadgets = Gadgets([], self.addresses)
 

gadgets/tests/test_shift_register_server.py

 class FakeSPI(object):
 
     def __init__(self, *args, **kw):
-        addresses = Addresses(in_port=port, out_port=port + 1)
-        self.sockets = Sockets(addresses)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
+        self.sockets = Sockets(self.addresses)
 
     def writebytes(self, value):
         self.sockets.send('fake spi', value)
         self.port = port
         self.channel = 2
         ShiftRegisterServer._SPI_Class = FakeSPI
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.sockets = Sockets(self.addresses, events=['fake spi'])
         self.server = ShiftRegisterServer(
             'null',

gadgets/tests/test_shift_register_switch.py

 class FakeSPI(object):
 
     def __init__(self, *args, **kw):
-        addresses = Addresses(in_port=port, out_port=port + 1)
-        self.sockets = Sockets(addresses)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
+        self.sockets = Sockets(self.addresses)
 
     def writebytes(self, value):
         self.sockets.send('fake spi', value)
         self.port = port
         self.channel = 2
         ShiftRegisterServer._SPI_Class = FakeSPI
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.sockets = Sockets(self.addresses, events=['fake spi'])
 
     def make_gadgets(self):

gadgets/tests/test_sockets.py

 class TestSockets(object):
 
     def test_broker(self):
-        p = random.randint(3000, 50000)
-        addresses = Addresses(in_port=p, out_port=p+1)
+        port = random.randint(3000, 50000)
+        addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         broker = Broker(addresses)
         sockets = Sockets(addresses)
         broker.start()

gadgets/tests/test_switch.py

 
     def setup(self):
         port = random.randint(5000, 50000)
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.sockets = Sockets(self.addresses, events=['update'])
         self.switch = Switch(
             'living room',

gadgets/tests/test_temperature_trigger.py

 import time
+import random
 from nose.tools import eq_
 from gadgets.devices.heater.triggers.temperature import TemperatureTrigger
 from gadgets import Addresses, Sockets, Broker
 class TestTemperatureTrigger(object):
 
     def setup(self):
-        self.addresses = Addresses()
+        port = random.randint(5000, 50000)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         comparitor = lambda x, y: x >= y
         self.trigger = TemperatureTrigger(
             'tank',

gadgets/tests/test_thermometer.py

-import time, threading, random, uuid, tempfile, os
+import time, threading, random, uuid, tempfile, os, platform
 from nose.tools import eq_
 from gadgets import Addresses, get_gadgets, Sockets
 from gadgets.sensors import Thermometer
 
     def setup(self):
         port = random.randint(5000, 50000)
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.sockets = None
         self.thermometer = Thermometer('living room', 'temperature', self.addresses, uid='x')
 
         self.sockets.send('status', {'id': 'test thermometer'})
         event, message = self.sockets.recv()
         eq_(event, 'test thermometer status')
+        name = platform.node()
         expected = {
+            u'name': name,
             u'errors': [],
-            u'name': u'pjoe',
+            u'name': name,
             u'sender': 'living room temperature',
             u'locations': {
                 u'living room': {

gadgets/tests/test_valve.py

     def setup(self):
         self._off = False
         port = random.randint(5000, 50000)
-        self.addresses = Addresses(in_port=port, out_port=port + 1)
+        self.addresses = Addresses(in_port=port, out_port=port+1, req_port=port+2)
         self.uid = str(uuid.uuid1())
         self.sockets = Sockets(self.addresses, events=[self.uid])
 
     if args.command:
         sockets = Sockets(addresses)
         sockets.send(args.command)
+        time.sleep(0.5)
         sockets.close()
+        return
     elif os.path.exists(config_path):
        config = imp.load_source(args.host, config_path)
        locations = config.locations
 from setuptools import setup, find_packages
 import sys, os
 
-version = '0.3.0'
+version = '0.3.1'
 
 long_description = ''
 
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.