overcode 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
overcode/tmux_manager.py CHANGED
@@ -1,11 +1,17 @@
1
1
  """
2
2
  Tmux session and window management for Overcode.
3
+
4
+ Uses libtmux for reliable tmux interaction.
3
5
  """
4
6
 
5
7
  import os
6
- import subprocess
8
+ import time
7
9
  from typing import Optional, List, Dict, Any, TYPE_CHECKING
8
10
 
11
+ import libtmux
12
+ from libtmux.exc import LibTmuxException
13
+ from libtmux._internal.query_list import ObjectDoesNotExist
14
+
9
15
  if TYPE_CHECKING:
10
16
  from .interfaces import TmuxInterface
11
17
 
@@ -13,7 +19,7 @@ if TYPE_CHECKING:
13
19
  class TmuxManager:
14
20
  """Manages tmux sessions and windows for Overcode.
15
21
 
16
- This class can be used directly (uses subprocess) or with an injected
22
+ This class can be used directly (uses libtmux) or with an injected
17
23
  TmuxInterface for testing.
18
24
  """
19
25
 
@@ -26,17 +32,44 @@ class TmuxManager:
26
32
  socket: Optional tmux socket name (for testing isolation)
27
33
  """
28
34
  self.session_name = session_name
29
- self._tmux = tmux # If None, use direct subprocess calls
35
+ self._tmux = tmux # If None, use libtmux directly
30
36
  # Support OVERCODE_TMUX_SOCKET env var for testing
31
37
  self.socket = socket or os.environ.get("OVERCODE_TMUX_SOCKET")
38
+ self._server: Optional[libtmux.Server] = None
39
+
40
+ @property
41
+ def server(self) -> libtmux.Server:
42
+ """Lazy-load the tmux server connection."""
43
+ if self._server is None:
44
+ if self.socket:
45
+ self._server = libtmux.Server(socket_name=self.socket)
46
+ else:
47
+ self._server = libtmux.Server()
48
+ return self._server
49
+
50
+ def _get_session(self) -> Optional[libtmux.Session]:
51
+ """Get the managed session, or None if it doesn't exist."""
52
+ try:
53
+ return self.server.sessions.get(session_name=self.session_name)
54
+ except (LibTmuxException, ObjectDoesNotExist):
55
+ return None
56
+
57
+ def _get_window(self, window_index: int) -> Optional[libtmux.Window]:
58
+ """Get a window by index."""
59
+ sess = self._get_session()
60
+ if sess is None:
61
+ return None
62
+ try:
63
+ return sess.windows.get(window_index=str(window_index))
64
+ except (LibTmuxException, ObjectDoesNotExist):
65
+ return None
32
66
 
33
- def _tmux_cmd(self, *args) -> List[str]:
34
- """Build tmux command with optional socket."""
35
- cmd = ["tmux"]
36
- if self.socket:
37
- cmd.extend(["-L", self.socket])
38
- cmd.extend(args)
39
- return cmd
67
+ def _get_pane(self, window_index: int) -> Optional[libtmux.Pane]:
68
+ """Get the first pane of a window."""
69
+ win = self._get_window(window_index)
70
+ if win is None or not win.panes:
71
+ return None
72
+ return win.panes[0]
40
73
 
41
74
  def ensure_session(self) -> bool:
42
75
  """Create tmux session if it doesn't exist"""
@@ -47,12 +80,9 @@ class TmuxManager:
47
80
  return self._tmux.new_session(self.session_name)
48
81
 
49
82
  try:
50
- subprocess.run(
51
- self._tmux_cmd("new-session", "-d", "-s", self.session_name),
52
- check=True
53
- )
83
+ self.server.new_session(session_name=self.session_name, attach=False)
54
84
  return True
55
- except subprocess.CalledProcessError:
85
+ except LibTmuxException:
56
86
  return False
57
87
 
58
88
  def session_exists(self) -> bool:
@@ -60,11 +90,10 @@ class TmuxManager:
60
90
  if self._tmux:
61
91
  return self._tmux.has_session(self.session_name)
62
92
 
63
- result = subprocess.run(
64
- self._tmux_cmd("has-session", "-t", self.session_name),
65
- capture_output=True
66
- )
67
- return result.returncode == 0
93
+ try:
94
+ return self.server.has_session(self.session_name)
95
+ except LibTmuxException:
96
+ return False
68
97
 
69
98
  def create_window(self, window_name: str, start_directory: Optional[str] = None) -> Optional[int]:
70
99
  """Create a new window in the tmux session"""
@@ -74,21 +103,18 @@ class TmuxManager:
74
103
  if self._tmux:
75
104
  return self._tmux.new_window(self.session_name, window_name, cwd=start_directory)
76
105
 
77
- args = [
78
- "new-window",
79
- "-t", self.session_name,
80
- "-n", window_name,
81
- "-P", # print window info
82
- "-F", "#{window_index}"
83
- ]
106
+ try:
107
+ sess = self._get_session()
108
+ if sess is None:
109
+ return None
84
110
 
85
- if start_directory:
86
- args.extend(["-c", start_directory])
111
+ kwargs: Dict[str, Any] = {'window_name': window_name, 'attach': False}
112
+ if start_directory:
113
+ kwargs['start_directory'] = start_directory
87
114
 
88
- try:
89
- result = subprocess.run(self._tmux_cmd(*args), capture_output=True, text=True, check=True)
90
- return int(result.stdout.strip())
91
- except (subprocess.CalledProcessError, ValueError):
115
+ window = sess.new_window(**kwargs)
116
+ return int(window.window_index)
117
+ except (LibTmuxException, ValueError):
92
118
  return None
93
119
 
94
120
  def send_keys(self, window_index: int, keys: str, enter: bool = True) -> bool:
@@ -97,31 +123,26 @@ class TmuxManager:
97
123
  For Claude Code: text and Enter must be sent as SEPARATE commands
98
124
  with a small delay, otherwise Claude Code doesn't process the Enter.
99
125
  """
100
- import time
101
-
102
126
  if self._tmux:
103
127
  return self._tmux.send_keys(self.session_name, window_index, keys, enter)
104
128
 
105
- target = f"{self.session_name}:{window_index}"
106
-
107
129
  try:
130
+ pane = self._get_pane(window_index)
131
+ if pane is None:
132
+ return False
133
+
108
134
  # Send text first (if any)
109
135
  if keys:
110
- subprocess.run(
111
- self._tmux_cmd("send-keys", "-t", target, keys),
112
- check=True
113
- )
136
+ pane.send_keys(keys, enter=False)
114
137
  # Small delay for Claude Code to process text
115
138
  time.sleep(0.1)
116
139
 
117
140
  # Send Enter separately
118
141
  if enter:
119
- subprocess.run(
120
- self._tmux_cmd("send-keys", "-t", target, "Enter"),
121
- check=True
122
- )
142
+ pane.send_keys('', enter=True)
143
+
123
144
  return True
124
- except subprocess.CalledProcessError:
145
+ except LibTmuxException:
125
146
  return False
126
147
 
127
148
  def attach_session(self):
@@ -129,7 +150,7 @@ class TmuxManager:
129
150
  if self._tmux:
130
151
  self._tmux.attach(self.session_name)
131
152
  return
132
- subprocess.run(self._tmux_cmd("attach", "-t", self.session_name))
153
+ os.execlp("tmux", "tmux", "attach-session", "-t", self.session_name)
133
154
 
134
155
  def list_windows(self) -> List[Dict[str, Any]]:
135
156
  """List all windows in the session.
@@ -148,31 +169,23 @@ class TmuxManager:
148
169
  ]
149
170
 
150
171
  try:
151
- result = subprocess.run(
152
- self._tmux_cmd(
153
- "list-windows",
154
- "-t", self.session_name,
155
- "-F", "#{window_index}|#{window_name}|#{pane_current_command}"
156
- ),
157
- capture_output=True, text=True, check=True
158
- )
172
+ sess = self._get_session()
173
+ if sess is None:
174
+ return []
159
175
 
160
176
  windows = []
161
- for line in result.stdout.strip().split("\n"):
162
- if line:
163
- parts = line.split("|")
164
- if len(parts) >= 3:
165
- try:
166
- window_index = int(parts[0])
167
- except ValueError:
168
- window_index = 0
169
- windows.append({
170
- "index": window_index,
171
- "name": parts[1],
172
- "command": parts[2]
173
- })
177
+ for win in sess.windows:
178
+ # Get command from first pane
179
+ command = ""
180
+ if win.panes:
181
+ command = win.panes[0].pane_current_command or ""
182
+ windows.append({
183
+ "index": int(win.window_index),
184
+ "name": win.window_name,
185
+ "command": command
186
+ })
174
187
  return windows
175
- except subprocess.CalledProcessError:
188
+ except LibTmuxException:
176
189
  return []
177
190
 
178
191
  def kill_window(self, window_index: int) -> bool:
@@ -181,12 +194,12 @@ class TmuxManager:
181
194
  return self._tmux.kill_window(self.session_name, window_index)
182
195
 
183
196
  try:
184
- subprocess.run(
185
- self._tmux_cmd("kill-window", "-t", f"{self.session_name}:{window_index}"),
186
- check=True
187
- )
197
+ win = self._get_window(window_index)
198
+ if win is None:
199
+ return False
200
+ win.kill()
188
201
  return True
189
- except subprocess.CalledProcessError:
202
+ except LibTmuxException:
190
203
  return False
191
204
 
192
205
  def kill_session(self) -> bool:
@@ -195,12 +208,12 @@ class TmuxManager:
195
208
  return self._tmux.kill_session(self.session_name)
196
209
 
197
210
  try:
198
- subprocess.run(
199
- self._tmux_cmd("kill-session", "-t", self.session_name),
200
- check=True
201
- )
211
+ sess = self._get_session()
212
+ if sess is None:
213
+ return False
214
+ sess.kill()
202
215
  return True
203
- except subprocess.CalledProcessError:
216
+ except LibTmuxException:
204
217
  return False
205
218
 
206
219
  def window_exists(self, window_index: int) -> bool:
@@ -213,16 +226,13 @@ class TmuxManager:
213
226
  return any(w.get('index') == window_index for w in windows)
214
227
 
215
228
  try:
216
- result = subprocess.run(
217
- self._tmux_cmd(
218
- "list-windows",
219
- "-t", self.session_name,
220
- "-F", "#{window_index}"
221
- ),
222
- capture_output=True, text=True, check=True
223
- )
224
-
225
- window_indices = [int(idx.strip()) for idx in result.stdout.strip().split("\n") if idx.strip()]
226
- return window_index in window_indices
227
- except (subprocess.CalledProcessError, ValueError):
229
+ sess = self._get_session()
230
+ if sess is None:
231
+ return False
232
+
233
+ for win in sess.windows:
234
+ if int(win.window_index) == window_index:
235
+ return True
236
+ return False
237
+ except LibTmuxException:
228
238
  return False