diff --git a/CHANGES b/CHANGES index 5090562d1b..ba718bfb58 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,15 @@ Here you can find the recent changes to tmuxp. - [config] :meth:`config.expand` now resolves directories in configuration via :py:func:`os.path.expanduser` and :py:func:`os.path.expandvars`. +- [config] :meth:`config.expandpath` for helping resolve paths. +- [builder] [cli] improved support for loading tmuxp project files from + outside current working directory. e.g. + + .. code-block:: bash + + $ tmuxp load /path/to/my/project/.tmuxp.yaml + + Will behave better with relative directories. 0.1.11 ------ diff --git a/doc/api.rst b/doc/api.rst index e9bc632609..019d2af0a3 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -98,6 +98,8 @@ Import and export .. automethod:: tmuxp.config.validate_schema +.. automethod:: tmuxp.config.expandpath + .. automethod:: tmuxp.config.expand .. automethod:: tmuxp.config.inline diff --git a/tmuxp/config.py b/tmuxp/config.py index e44873ed39..b11fc70830 100644 --- a/tmuxp/config.py +++ b/tmuxp/config.py @@ -102,6 +102,18 @@ def in_cwd(): return configs +def expandpath(_path): + """Return expanded path based on user's ``$HOME`` and ``env``. + + :py:func:`os.path.expanduser` and :py:func:`os.path.expandvars` + + :param _path: path to expand + :type _path: string + :returns: expanded path + :rtype: string + + """ + return os.path.expandvars(os.path.expanduser(_path)) def inline(sconf): """ Return config in inline form, opposite of :meth:`config.expand`. @@ -139,7 +151,7 @@ def inline(sconf): return sconf -def expand(sconf, cwd=None): +def expand(sconf, cwd=None, parent=None): """Return config with shorthand and inline properties expanded. This is necessary to keep the code in the :class:`WorkspaceBuilder` clean @@ -172,11 +184,21 @@ def expand(sconf, cwd=None): # Any config section, session, window, pane that can contain the # 'shell_command' value if 'start_directory' in sconf: + sconf['start_directory'] = expandpath(sconf['start_directory']) start_path = sconf['start_directory'] if any(start_path.startswith(a) for a in ['.', './']): + # if window has a session, or pane has a window with a + # start_directory of . or ./, make sure the start_directory can be + # relative to the parent. + # + # This is for the case where you may be loading a config from + # outside your shell current directory. + if parent: + cwd = parent['start_directory'] start_path = os.path.normpath(os.path.join(cwd, start_path)) sconf['start_directory'] = start_path + if 'before_script' in sconf: before_script = sconf['before_script'] if any(before_script.startswith(a) for a in ['.', './']): @@ -198,7 +220,7 @@ def expand(sconf, cwd=None): # recurse into window and pane config items if 'windows' in sconf: sconf['windows'] = [ - expand(window) for window in sconf['windows'] + expand(window, parent=sconf) for window in sconf['windows'] ] elif 'panes' in sconf: @@ -235,7 +257,7 @@ def expand(sconf, cwd=None): p['shell_command'] = [] pconf.update(p) - sconf['panes'] = [expand(pane) for pane in sconf['panes']] + sconf['panes'] = [expand(pane, parent=sconf) for pane in sconf['panes']] return sconf diff --git a/tmuxp/testsuite/config.py b/tmuxp/testsuite/config.py index b7933743d6..e791557e90 100644 --- a/tmuxp/testsuite/config.py +++ b/tmuxp/testsuite/config.py @@ -212,7 +212,7 @@ class ExpandTest(TestCase): after_config = { 'session_name': 'sampleconfig', - 'start_directory': '~', + 'start_directory': os.path.expanduser('~'), 'windows': [ { 'window_name': 'editor', @@ -246,19 +246,19 @@ class ExpandTest(TestCase): ] }, { - 'start_directory': os.path.abspath('./'), + 'start_directory': os.path.normpath(os.path.join(os.path.join(os.path.expanduser('~'), './'))), 'panes': [ {'shell_command': ['pwd']} ] }, { - 'start_directory': os.path.abspath('./asdf/'), + 'start_directory': os.path.normpath(os.path.join(os.path.join(os.path.expanduser('~'), './asdf'))), 'panes': [ {'shell_command': ['pwd']} ] }, { - 'start_directory': os.path.abspath('../'), + 'start_directory': os.path.normpath(os.path.join(os.path.expanduser('~'), '../')), 'panes': [ {'shell_command': ['pwd']} ] @@ -317,7 +317,7 @@ def test_no_window_name(self): expanded_yaml = """ session_name: sampleconfig - start_directory: '~' + start_directory: {HOME} windows: - window_name: focused window layout: main-horizontal @@ -348,7 +348,9 @@ def test_no_window_name(self): focus: true - shell_command: [] - shell_command: [] - """ + """.format( + HOME=os.path.expanduser('~') + ) self.maxDiff = None @@ -622,7 +624,7 @@ class ShellCommandBeforeTest(TestCase): 'windows': [ { 'window_name': 'editor', - 'start_directory': '~', + 'start_directory': os.path.expanduser('~'), 'shell_command_before': ['source .env/bin/activate'], 'panes': [ { @@ -676,7 +678,7 @@ class ShellCommandBeforeTest(TestCase): 'windows': [ { 'window_name': 'editor', - 'start_directory': '~', + 'start_directory': os.path.expanduser('~'), 'shell_command_before': ['source .env/bin/activate'], 'panes': [ { diff --git a/tmuxp/testsuite/workspacebuilder.py b/tmuxp/testsuite/workspacebuilder.py index 2d18a69724..7432c89d70 100644 --- a/tmuxp/testsuite/workspacebuilder.py +++ b/tmuxp/testsuite/workspacebuilder.py @@ -432,6 +432,8 @@ def tearDown(self): def test_start_directory(self): + start_directory = os.getcwd() + sconfig = kaptan.Kaptan(handler='yaml') sconfig = sconfig.import_config(self.yaml_config).get() sconfig = config.expand(sconfig) @@ -441,7 +443,117 @@ def test_start_directory(self): builder.build(session=self.session) assert(self.session == builder.session) - dirs = ['/usr/bin', '/dev', '/tmp/foo bar', '/usr', os.getcwd()] + dirs = ['/usr/bin', '/dev', '/tmp/foo bar', '/usr', '/usr'] + for path, window in zip(dirs, self.session.windows): + for p in window.panes: + for i in range(60): + p.server._update_panes() + if p.get('pane_current_path') == path: + break + time.sleep(.2) + + self.assertEqual(p.get('pane_current_path'), path) + + +class StartDirectoryRelativeTest(TmuxTestCase): + """Same as above test, but with relative start directory, mimicing + loading it from a location of project file. Like:: + + $ tmuxp load ~/workspace/myproject/.tmuxp.yaml + + instead of:: + + $ cd ~/workspace/myproject/.tmuxp.yaml + $ tmuxp load . + + """ + + yaml_config = """ + session_name: sampleconfig + start_directory: ./ + windows: + - window_name: supposed to be /usr/bin + start_directory: '/usr/bin' + layout: main-horizontal + options: + main-pane-height: 50 + panes: + - shell_command: + - echo "hey" + - shell_command: + - echo "moo" + - window_name: support to be /dev + start_directory: '/dev' + layout: main-horizontal + panes: + - shell_command: + - pwd + - shell_command: + - echo "hey" + - shell_command: + - echo "moo" + - window_name: cwd containing a space + start_directory: /tmp/foo bar + layout: main-horizontal + panes: + - shell_command: + - pwd + - shell_command: + - echo "hey" + - shell_command: + - echo "moo" + - window_name: testsa3 + layout: main-horizontal + panes: + - shell_command: + - pwd + - shell_command: + - echo "hey" + - shell_command: + - echo "moo3" + - window_name: cwd relative to config file + layout: main-horizontal + start_directory: ./ + panes: + - shell_command: + - pwd + - shell_command: + - echo "hey" + - shell_command: + - echo "moo3" + """ + + def setUp(self): + super(StartDirectoryRelativeTest, self).setUp() + if not os.path.exists('/tmp/foo bar') and not os.path.exists('/tmp/testRelConfigDir'): + os.mkdir('/tmp/foo bar') + os.mkdir('/tmp/testRelConfigDir') + self._temp_dir_created = True + else: + self._temp_dir_created = False + + def tearDown(self): + super(StartDirectoryRelativeTest, self).tearDown() + if self._temp_dir_created: + os.rmdir('/tmp/foo bar') + os.rmdir('/tmp/testRelConfigDir') + + def test_start_directory(self): + + start_directory = os.getcwd() + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(self.yaml_config).get() + # the second argument of os.getcwd() mimics the behavior + # the CLI loader will do, but it passes in the config file's location. + sconfig = config.expand(sconfig, '/tmp/testRelConfigDir') + sconfig = config.trickle(sconfig) + + builder = WorkspaceBuilder(sconf=sconfig) + builder.build(session=self.session) + + assert(self.session == builder.session) + dirs = ['/usr/bin', '/dev', '/tmp/foo bar', '/tmp/testRelConfigDir'] for path, window in zip(dirs, self.session.windows): for p in window.panes: for i in range(60): @@ -660,6 +772,7 @@ def suite(): suite.addTest(unittest.makeSuite(FocusAndPaneIndexTest)) suite.addTest(unittest.makeSuite(PaneOrderingTest)) suite.addTest(unittest.makeSuite(StartDirectoryTest)) + suite.addTest(unittest.makeSuite(StartDirectoryRelativeTest)) suite.addTest(unittest.makeSuite(ThreePaneTest)) suite.addTest(unittest.makeSuite(TwoPaneTest)) suite.addTest(unittest.makeSuite(WindowAutomaticRename))