1"""Pythonization of the :term:`tmux(1)` server.
2
3libtmux.server
4~~~~~~~~~~~~~~
5
6"""
7import logging
8import os
9
10from . import exc, formats
11from .common import (
12    EnvironmentMixin,
13    TmuxRelationalObject,
14    has_gte_version,
15    session_check_name,
16    tmux_cmd,
17)
18from .session import Session
19
20logger = logging.getLogger(__name__)
21
22
23class Server(TmuxRelationalObject, EnvironmentMixin):
24
25    """
26    The :term:`tmux(1)` :term:`Server` [server_manual]_.
27
28    - :attr:`Server._sessions` [:class:`Session`, ...]
29
30      - :attr:`Session._windows` [:class:`Window`, ...]
31
32        - :attr:`Window._panes` [:class:`Pane`, ...]
33
34          - :class:`Pane`
35
36    When instantiated stores information on live, running tmux server.
37
38    Parameters
39    ----------
40    socket_name : str, optional
41    socket_path : str, optional
42    config_file : str, optional
43    colors : str, optional
44
45    References
46    ----------
47    .. [server_manual] CLIENTS AND SESSIONS. openbsd manpage for TMUX(1)
48           "The tmux server manages clients, sessions, windows and panes.
49           Clients are attached to sessions to interact with them, either when
50           they are created with the new-session command, or later with the
51           attach-session command. Each session has one or more windows linked
52           into it. Windows may be linked to multiple sessions and are made up
53           of one or more panes, each of which contains a pseudo terminal."
54
55       https://man.openbsd.org/tmux.1#CLIENTS_AND_SESSIONS.
56       Accessed April 1st, 2018.
57    """
58
59    #: ``[-L socket-name]``
60    socket_name = None
61    #: ``[-S socket-path]``
62    socket_path = None
63    #: ``[-f file]``
64    config_file = None
65    #: ``-2`` or ``-8``
66    colors = None
67    #: unique child ID used by :class:`~libtmux.common.TmuxRelationalObject`
68    child_id_attribute = 'session_id'
69    #: namespace used :class:`~libtmux.common.TmuxMappingObject`
70    formatter_prefix = 'server_'
71
72    def __init__(
73        self,
74        socket_name=None,
75        socket_path=None,
76        config_file=None,
77        colors=None,
78        **kwargs
79    ):
80        EnvironmentMixin.__init__(self, '-g')
81        self._windows = []
82        self._panes = []
83
84        if socket_name:
85            self.socket_name = socket_name
86
87        if socket_path:
88            self.socket_path = socket_path
89
90        if config_file:
91            self.config_file = config_file
92
93        if colors:
94            self.colors = colors
95
96    def cmd(self, *args, **kwargs):
97        """
98        Execute tmux command and return output.
99
100        Returns
101        -------
102        :class:`common.tmux_cmd`
103
104        Notes
105        -----
106        .. versionchanged:: 0.8
107
108            Renamed from ``.tmux`` to ``.cmd``.
109        """
110
111        args = list(args)
112        if self.socket_name:
113            args.insert(0, '-L{}'.format(self.socket_name))
114        if self.socket_path:
115            args.insert(0, '-S{}'.format(self.socket_path))
116        if self.config_file:
117            args.insert(0, '-f{}'.format(self.config_file))
118        if self.colors:
119            if self.colors == 256:
120                args.insert(0, '-2')
121            elif self.colors == 88:
122                args.insert(0, '-8')
123            else:
124                raise ValueError('Server.colors must equal 88 or 256')
125
126        return tmux_cmd(*args, **kwargs)
127
128    def _list_sessions(self):
129        """
130        Return list of sessions in :py:obj:`dict` form.
131
132        Retrieved from ``$ tmux(1) list-sessions`` stdout.
133
134        The :py:obj:`list` is derived from ``stdout`` in
135        :class:`common.tmux_cmd` which wraps :py:class:`subprocess.Popen`.
136
137        Returns
138        -------
139        list of dict
140        """
141
142        sformats = formats.SESSION_FORMATS
143        tmux_formats = ['#{%s}' % f for f in sformats]
144
145        tmux_args = ('-F%s' % '\t'.join(tmux_formats),)  # output
146
147        proc = self.cmd('list-sessions', *tmux_args)
148
149        if proc.stderr:
150            raise exc.LibTmuxException(proc.stderr)
151
152        sformats = formats.SESSION_FORMATS
153        tmux_formats = ['#{%s}' % format for format in sformats]
154        sessions = proc.stdout
155
156        # combine format keys with values returned from ``tmux list-sessions``
157        sessions = [dict(zip(sformats, session.split('\t'))) for session in sessions]
158
159        # clear up empty dict
160        sessions = [
161            dict((k, v) for k, v in session.items() if v) for session in sessions
162        ]
163
164        return sessions
165
166    @property
167    def _sessions(self):
168        """Property / alias to return :meth:`~._list_sessions`."""
169
170        return self._list_sessions()
171
172    def list_sessions(self):
173        """
174        Return list of :class:`Session` from the ``tmux(1)`` session.
175
176        Returns
177        -------
178        list of :class:`Session`
179        """
180        return [Session(server=self, **s) for s in self._sessions]
181
182    @property
183    def sessions(self):
184        """Property / alias to return :meth:`~.list_sessions`."""
185        return self.list_sessions()
186
187    #: Alias :attr:`sessions` for :class:`~libtmux.common.TmuxRelationalObject`
188    children = sessions
189
190    def _list_windows(self):
191        """
192        Return list of windows in :py:obj:`dict` form.
193
194        Retrieved from ``$ tmux(1) list-windows`` stdout.
195
196        The :py:obj:`list` is derived from ``stdout`` in
197        :class:`common.tmux_cmd` which wraps :py:class:`subprocess.Popen`.
198
199        Returns
200        -------
201        list of dict
202        """
203
204        wformats = ['session_name', 'session_id'] + formats.WINDOW_FORMATS
205        tmux_formats = ['#{%s}' % format for format in wformats]
206
207        proc = self.cmd(
208            'list-windows',  # ``tmux list-windows``
209            '-a',
210            '-F%s' % '\t'.join(tmux_formats),  # output
211        )
212
213        if proc.stderr:
214            raise exc.LibTmuxException(proc.stderr)
215
216        windows = proc.stdout
217
218        wformats = ['session_name', 'session_id'] + formats.WINDOW_FORMATS
219
220        # combine format keys with values returned from ``tmux list-windows``
221        windows = [dict(zip(wformats, window.split('\t'))) for window in windows]
222
223        # clear up empty dict
224        windows = [dict((k, v) for k, v in window.items() if v) for window in windows]
225
226        # tmux < 1.8 doesn't have window_id, use window_name
227        for w in windows:
228            if 'window_id' not in w:
229                w['window_id'] = w['window_name']
230
231        if self._windows:
232            self._windows[:] = []
233
234        self._windows.extend(windows)
235
236        return self._windows
237
238    def _update_windows(self):
239        """
240        Update internal window data and return ``self`` for chainability.
241
242        Returns
243        -------
244        :class:`Server`
245        """
246        self._list_windows()
247        return self
248
249    def _list_panes(self):
250        """
251        Return list of panes in :py:obj:`dict` form.
252
253        Retrieved from ``$ tmux(1) list-panes`` stdout.
254
255        The :py:obj:`list` is derived from ``stdout`` in
256        :class:`util.tmux_cmd` which wraps :py:class:`subprocess.Popen`.
257
258        Returns
259        -------
260        list
261        """
262
263        pformats = [
264            'session_name',
265            'session_id',
266            'window_index',
267            'window_id',
268            'window_name',
269        ] + formats.PANE_FORMATS
270        tmux_formats = ['#{%s}\t' % f for f in pformats]
271
272        proc = self.cmd('list-panes', '-a', '-F%s' % ''.join(tmux_formats))  # output
273
274        if proc.stderr:
275            raise exc.LibTmuxException(proc.stderr)
276
277        panes = proc.stdout
278
279        pformats = [
280            'session_name',
281            'session_id',
282            'window_index',
283            'window_id',
284            'window_name',
285        ] + formats.PANE_FORMATS
286
287        # combine format keys with values returned from ``tmux list-panes``
288        panes = [dict(zip(pformats, window.split('\t'))) for window in panes]
289
290        # clear up empty dict
291        panes = [
292            dict(
293                (k, v) for k, v in window.items() if v or k == 'pane_current_path'
294            )  # preserve pane_current_path, in case it entered a new process
295            # where we may not get a cwd from.
296            for window in panes
297        ]
298
299        if self._panes:
300            self._panes[:] = []
301
302        self._panes.extend(panes)
303
304        return self._panes
305
306    def _update_panes(self):
307        """
308        Update internal pane data and return ``self`` for chainability.
309
310        Returns
311        -------
312        :class:`Server`
313        """
314        self._list_panes()
315        return self
316
317    @property
318    def attached_sessions(self):
319        """
320        Return active :class:`Session` objects.
321
322        Returns
323        -------
324        list of :class:`Session`
325        """
326
327        sessions = self._sessions
328        attached_sessions = list()
329
330        for session in sessions:
331            attached = session.get('session_attached')
332            # for now session_active is a unicode
333            if attached != '0':
334                logger.debug('session %s attached', session.get('name'))
335                attached_sessions.append(session)
336            else:
337                continue
338
339        return [Session(server=self, **s) for s in attached_sessions] or None
340
341    def has_session(self, target_session, exact=True):
342        """
343        Return True if session exists. ``$ tmux has-session``.
344
345        Parameters
346        ----------
347        target_session : str
348            session name
349        exact : bool
350            match the session name exactly. tmux uses fnmatch by default.
351            Internally prepends ``=`` to the session in ``$ tmux has-session``.
352            tmux 2.1 and up only.
353
354        Raises
355        ------
356        :exc:`exc.BadSessionName`
357
358        Returns
359        -------
360        bool
361        """
362        session_check_name(target_session)
363
364        if exact and has_gte_version('2.1'):
365            target_session = '={}'.format(target_session)
366
367        proc = self.cmd('has-session', '-t%s' % target_session)
368
369        if not proc.returncode:
370            return True
371
372        return False
373
374    def kill_server(self):
375        """``$ tmux kill-server``."""
376        self.cmd('kill-server')
377
378    def kill_session(self, target_session=None):
379        """
380        Kill the tmux session with ``$ tmux kill-session``, return ``self``.
381
382        Parameters
383        ----------
384        target_session : str, optional
385            target_session: str. note this accepts ``fnmatch(3)``. 'asdf' will
386            kill 'asdfasd'.
387
388        Returns
389        -------
390        :class:`Server`
391
392        Raises
393        ------
394        :exc:`exc.BadSessionName`
395        """
396        session_check_name(target_session)
397
398        proc = self.cmd('kill-session', '-t%s' % target_session)
399
400        if proc.stderr:
401            raise exc.LibTmuxException(proc.stderr)
402
403        return self
404
405    def switch_client(self, target_session):
406        """
407        ``$ tmux switch-client``.
408
409        Parameters
410        ----------
411        target_session : str
412            name of the session. fnmatch(3) works.
413
414        Raises
415        ------
416        :exc:`exc.BadSessionName`
417        """
418        session_check_name(target_session)
419
420        proc = self.cmd('switch-client', '-t%s' % target_session)
421
422        if proc.stderr:
423            raise exc.LibTmuxException(proc.stderr)
424
425    def attach_session(self, target_session=None):
426        """``$ tmux attach-session`` aka alias: ``$ tmux attach``.
427
428        Parameters
429        ----------
430        target_session : str
431            name of the session. fnmatch(3) works.
432
433        Raises
434        ------
435        :exc:`exc.BadSessionName`
436        """
437        session_check_name(target_session)
438
439        tmux_args = tuple()
440        if target_session:
441            tmux_args += ('-t%s' % target_session,)
442
443        proc = self.cmd('attach-session', *tmux_args)
444
445        if proc.stderr:
446            raise exc.LibTmuxException(proc.stderr)
447
448    def new_session(
449        self,
450        session_name=None,
451        kill_session=False,
452        attach=False,
453        start_directory=None,
454        window_name=None,
455        window_command=None,
456        *args,
457        **kwargs
458    ):
459        """
460        Return :class:`Session` from  ``$ tmux new-session``.
461
462        Uses ``-P`` flag to print session info, ``-F`` for return formatting
463        returns new Session object.
464
465        ``$ tmux new-session -d`` will create the session in the background
466        ``$ tmux new-session -Ad`` will move to the session name if it already
467        exists. todo: make an option to handle this.
468
469        Parameters
470        ----------
471        session_name : str, optional
472            ::
473
474                $ tmux new-session -s <session_name>
475        attach : bool, optional
476            create session in the foreground. ``attach=False`` is equivalent
477            to::
478
479                $ tmux new-session -d
480
481        Other Parameters
482        ----------------
483        kill_session : bool, optional
484            Kill current session if ``$ tmux has-session``.
485            Useful for testing workspaces.
486        start_directory : str, optional
487            specifies the working directory in which the
488            new session is created.
489        window_name : str, optional
490            ::
491
492                $ tmux new-session -n <window_name>
493        window_command : str
494            execute a command on starting the session.  The window will close
495            when the command exits. NOTE: When this command exits the window
496            will close.  This feature is useful for long-running processes
497            where the closing of the window upon completion is desired.
498
499        Returns
500        -------
501        :class:`Session`
502
503        Raises
504        ------
505        :exc:`exc.BadSessionName`
506        """
507        session_check_name(session_name)
508
509        if self.has_session(session_name):
510            if kill_session:
511                self.cmd('kill-session', '-t%s' % session_name)
512                logger.info('session %s exists. killed it.' % session_name)
513            else:
514                raise exc.TmuxSessionExists('Session named %s exists' % session_name)
515
516        logger.debug('creating session %s' % session_name)
517
518        sformats = formats.SESSION_FORMATS
519        tmux_formats = ['#{%s}' % f for f in sformats]
520
521        env = os.environ.get('TMUX')
522
523        if env:
524            del os.environ['TMUX']
525
526        tmux_args = (
527            '-s%s' % session_name,
528            '-P',
529            '-F%s' % '\t'.join(tmux_formats),  # output
530        )
531
532        if not attach:
533            tmux_args += ('-d',)
534
535        if start_directory:
536            tmux_args += ('-c', start_directory)
537
538        if window_name:
539            tmux_args += ('-n', window_name)
540
541        # tmux 2.6 gives unattached sessions a tiny default area
542        # no need send in -x/-y if they're in a client already, though
543        if has_gte_version('2.6') and 'TMUX' not in os.environ:
544            tmux_args += ('-x', 800, '-y', 600)
545
546        if window_command:
547            tmux_args += (window_command,)
548
549        proc = self.cmd('new-session', *tmux_args)
550
551        if proc.stderr:
552            raise exc.LibTmuxException(proc.stderr)
553
554        session = proc.stdout[0]
555
556        if env:
557            os.environ['TMUX'] = env
558
559        # combine format keys with values returned from ``tmux list-windows``
560        session = dict(zip(sformats, session.split('\t')))
561
562        # clear up empty dict
563        session = dict((k, v) for k, v in session.items() if v)
564
565        session = Session(server=self, **session)
566
567        return session
568