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