optmux 0.1.0__tar.gz
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.
- optmux-0.1.0/PKG-INFO +213 -0
- optmux-0.1.0/README.md +202 -0
- optmux-0.1.0/optmux/__init__.py +1 -0
- optmux-0.1.0/optmux/cli.py +149 -0
- optmux-0.1.0/optmux/data/plugins-update.sh +25 -0
- optmux-0.1.0/optmux/data/tips.sh +76 -0
- optmux-0.1.0/optmux/data/tmux.conf +141 -0
- optmux-0.1.0/pyproject.toml +23 -0
optmux-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: optmux
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Optimal, opinionated, batteries-included TMUX that's neat and easy for any project
|
|
5
|
+
Author: Jaeho Shin
|
|
6
|
+
Author-email: Jaeho Shin <netj@sparcs.org>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: tmuxp
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# optmux
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="optmux.svg" width="192" alt="optmux logo">
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
Optimal, opinionated, batteries-included TMUX that's neat and easy for any project.
|
|
19
|
+
|
|
20
|
+
A [tmuxp](https://tmuxp.git-pull.com) wrapper that creates per-project tmux config directories with [TPM](https://github.com/tmux-plugins/tpm) and plugins pre-configured.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# run optmux anywhere (installs on first use via uv)
|
|
26
|
+
uvx optmux
|
|
27
|
+
|
|
28
|
+
# strongly recommended: install wtcode + lazygit for the full experience
|
|
29
|
+
brew install netj/tap/optmux
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Try the included example:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/netj/optmux.git && cd optmux
|
|
36
|
+
./example.optmux.yaml
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
On first run, optmux will:
|
|
40
|
+
|
|
41
|
+
1. Create `.example.optmux.d/tmux/` next to the YAML file
|
|
42
|
+
2. Seed a default `tmux.conf` with TPM and plugins
|
|
43
|
+
3. Install TPM and all plugins (visible in window 0)
|
|
44
|
+
4. Launch tmuxp with an isolated tmux server
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
### With a tmuxp YAML file
|
|
49
|
+
|
|
50
|
+
Supports `.optmux.yaml`, `.tmuxp.yaml`, and `.optmuxp.yaml` extensions:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
optmux myproject.optmux.yaml
|
|
54
|
+
optmux myproject.tmuxp.yaml
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Without arguments
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
optmux
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Opens plain `tmux` using `.optmux.d/` in the current directory — useful for a quick, isolated tmux session with the bundled config.
|
|
64
|
+
|
|
65
|
+
### As a shebang
|
|
66
|
+
|
|
67
|
+
Write a [tmuxp YAML config](https://tmuxp.git-pull.com/configuration/) with the optmux shebang line and make it executable:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
#!/usr/bin/env -S uvx optmux
|
|
71
|
+
session_name: myproject
|
|
72
|
+
windows:
|
|
73
|
+
- window_name: editor
|
|
74
|
+
panes:
|
|
75
|
+
- vim .
|
|
76
|
+
- window_name: shell
|
|
77
|
+
panes:
|
|
78
|
+
- ""
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
chmod +x myproject.optmux.yaml
|
|
83
|
+
./myproject.optmux.yaml
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Example tmuxp YAML
|
|
87
|
+
|
|
88
|
+
Here's the included [`example.optmux.yaml`](example.optmux.yaml) showing shortcuts, tmux config, and window layout:
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
#!/usr/bin/env -S uvx optmux
|
|
92
|
+
session_name: example
|
|
93
|
+
start_directory: .
|
|
94
|
+
|
|
95
|
+
optmux:
|
|
96
|
+
shortcuts:
|
|
97
|
+
C-M-b: gh browse .
|
|
98
|
+
C-M-e:
|
|
99
|
+
command: ${VISUAL:-${EDITOR:-vim}} README.md # exec directly (default for str, no latency)
|
|
100
|
+
window: true # in a new-window
|
|
101
|
+
E:
|
|
102
|
+
send-keys: ${VISUAL:-${EDITOR:-vim}} . # send-keys (given command is run in a new shell)
|
|
103
|
+
zoom: false # do not zoom (defaults to zoom when split-window)
|
|
104
|
+
tmux_config:
|
|
105
|
+
project-settings: |
|
|
106
|
+
set -g status-style bg=blue
|
|
107
|
+
|
|
108
|
+
windows:
|
|
109
|
+
- window_name: editor
|
|
110
|
+
panes:
|
|
111
|
+
- vim .
|
|
112
|
+
- window_name: shell
|
|
113
|
+
panes:
|
|
114
|
+
- ""
|
|
115
|
+
- window_name: logs
|
|
116
|
+
panes:
|
|
117
|
+
- tail -f /var/log/system.log
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Config directory
|
|
121
|
+
|
|
122
|
+
Each project gets its own `.$NAME.optmux.d/` directory:
|
|
123
|
+
|
|
124
|
+
| Path | Purpose |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `tmux/tmux.conf` | Main tmux config (editable after creation) |
|
|
127
|
+
| `tmux/tmux.*.conf` | Additional config files you can add |
|
|
128
|
+
| `tmux/tmux.sock` | Tmux server socket (isolates this project) |
|
|
129
|
+
| `tmux/plugins/` | TPM plugin directory |
|
|
130
|
+
| `tmux/plugins-update.sh` | Run manually to update all plugins |
|
|
131
|
+
|
|
132
|
+
## optmux YAML config
|
|
133
|
+
|
|
134
|
+
Add an `optmux:` section to your tmuxp YAML to configure shortcuts and tmux settings:
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
optmux:
|
|
138
|
+
shortcuts:
|
|
139
|
+
C-M-b: gh browse . # Ctrl-Alt-b: run command directly
|
|
140
|
+
C-M-e:
|
|
141
|
+
command: ${VISUAL:-${EDITOR:-vim}} README.md # exec directly (no shell)
|
|
142
|
+
window: true # open in a new-window
|
|
143
|
+
E:
|
|
144
|
+
send-keys: ${VISUAL:-${EDITOR:-vim}} . # send-keys (runs in a new shell)
|
|
145
|
+
zoom: false # do not zoom (default: true for splits)
|
|
146
|
+
tmux_config:
|
|
147
|
+
project-settings: |
|
|
148
|
+
set -g status-style bg=blue
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Shortcuts
|
|
152
|
+
|
|
153
|
+
Shortcuts bind tmux keys to commands:
|
|
154
|
+
|
|
155
|
+
- **`C-M-*` keys** are bound globally (no prefix needed)
|
|
156
|
+
- **Other keys** require the tmux prefix (`C-t`)
|
|
157
|
+
- **`command:`** executes directly (default for string values)
|
|
158
|
+
- **`send-keys:`** sends the command to a new shell (supports shell expansion)
|
|
159
|
+
- **`window: true`** opens in a new window instead of a split
|
|
160
|
+
- **`zoom: false`** disables auto-zoom on splits (default: true)
|
|
161
|
+
|
|
162
|
+
### tmux_config
|
|
163
|
+
|
|
164
|
+
Entries under `tmux_config:` are written as `tmux.optmux-extras.{name}.conf` files and auto-sourced by tmux.
|
|
165
|
+
|
|
166
|
+
### Personal config (`~/.optmux.yaml`)
|
|
167
|
+
|
|
168
|
+
Create `~/.optmux.yaml` to define personal defaults that apply to all optmux sessions:
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
optmux:
|
|
172
|
+
shortcuts:
|
|
173
|
+
C-M-g: lazygit
|
|
174
|
+
tmux_config:
|
|
175
|
+
my-defaults: |
|
|
176
|
+
set -g status-style bg=green
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Personal config is merged with per-project config. When both define the same key, **personal settings take precedence**.
|
|
180
|
+
|
|
181
|
+
### Customization
|
|
182
|
+
|
|
183
|
+
- Edit `tmux/tmux.conf` to change tmux settings
|
|
184
|
+
- Drop `tmux/tmux.mysetup.conf` files for additional config (auto-sourced)
|
|
185
|
+
- Run `tmux/plugins-update.sh` from inside tmux to update plugins
|
|
186
|
+
- Press `prefix + R` to reload the config
|
|
187
|
+
|
|
188
|
+
### Environment variables
|
|
189
|
+
|
|
190
|
+
optmux sets these before launching tmux/tmuxp:
|
|
191
|
+
|
|
192
|
+
| Variable | Value |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `OPTMUX_DIR` | Absolute path to the `.$NAME.optmux.d/` directory |
|
|
195
|
+
| `OPTMUX_NAME` | Name derived from YAML filename or cwd (e.g., `myproject`) |
|
|
196
|
+
| `TMUX_PLUGIN_MANAGER_PATH` | `$OPTMUX_DIR/tmux/plugins` |
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# install the latest main branch
|
|
202
|
+
uvx git+https://github.com/netj/optmux.git
|
|
203
|
+
|
|
204
|
+
# local editable install for development
|
|
205
|
+
uv tool install -e .
|
|
206
|
+
|
|
207
|
+
# test any local changes directly (best for testing branches)
|
|
208
|
+
uv run optmux ./example.optmux.yaml
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
[MIT](LICENSE)
|
optmux-0.1.0/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# optmux
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="optmux.svg" width="192" alt="optmux logo">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
Optimal, opinionated, batteries-included TMUX that's neat and easy for any project.
|
|
8
|
+
|
|
9
|
+
A [tmuxp](https://tmuxp.git-pull.com) wrapper that creates per-project tmux config directories with [TPM](https://github.com/tmux-plugins/tpm) and plugins pre-configured.
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# run optmux anywhere (installs on first use via uv)
|
|
15
|
+
uvx optmux
|
|
16
|
+
|
|
17
|
+
# strongly recommended: install wtcode + lazygit for the full experience
|
|
18
|
+
brew install netj/tap/optmux
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Try the included example:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/netj/optmux.git && cd optmux
|
|
25
|
+
./example.optmux.yaml
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
On first run, optmux will:
|
|
29
|
+
|
|
30
|
+
1. Create `.example.optmux.d/tmux/` next to the YAML file
|
|
31
|
+
2. Seed a default `tmux.conf` with TPM and plugins
|
|
32
|
+
3. Install TPM and all plugins (visible in window 0)
|
|
33
|
+
4. Launch tmuxp with an isolated tmux server
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### With a tmuxp YAML file
|
|
38
|
+
|
|
39
|
+
Supports `.optmux.yaml`, `.tmuxp.yaml`, and `.optmuxp.yaml` extensions:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
optmux myproject.optmux.yaml
|
|
43
|
+
optmux myproject.tmuxp.yaml
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Without arguments
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
optmux
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Opens plain `tmux` using `.optmux.d/` in the current directory — useful for a quick, isolated tmux session with the bundled config.
|
|
53
|
+
|
|
54
|
+
### As a shebang
|
|
55
|
+
|
|
56
|
+
Write a [tmuxp YAML config](https://tmuxp.git-pull.com/configuration/) with the optmux shebang line and make it executable:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
#!/usr/bin/env -S uvx optmux
|
|
60
|
+
session_name: myproject
|
|
61
|
+
windows:
|
|
62
|
+
- window_name: editor
|
|
63
|
+
panes:
|
|
64
|
+
- vim .
|
|
65
|
+
- window_name: shell
|
|
66
|
+
panes:
|
|
67
|
+
- ""
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
chmod +x myproject.optmux.yaml
|
|
72
|
+
./myproject.optmux.yaml
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Example tmuxp YAML
|
|
76
|
+
|
|
77
|
+
Here's the included [`example.optmux.yaml`](example.optmux.yaml) showing shortcuts, tmux config, and window layout:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
#!/usr/bin/env -S uvx optmux
|
|
81
|
+
session_name: example
|
|
82
|
+
start_directory: .
|
|
83
|
+
|
|
84
|
+
optmux:
|
|
85
|
+
shortcuts:
|
|
86
|
+
C-M-b: gh browse .
|
|
87
|
+
C-M-e:
|
|
88
|
+
command: ${VISUAL:-${EDITOR:-vim}} README.md # exec directly (default for str, no latency)
|
|
89
|
+
window: true # in a new-window
|
|
90
|
+
E:
|
|
91
|
+
send-keys: ${VISUAL:-${EDITOR:-vim}} . # send-keys (given command is run in a new shell)
|
|
92
|
+
zoom: false # do not zoom (defaults to zoom when split-window)
|
|
93
|
+
tmux_config:
|
|
94
|
+
project-settings: |
|
|
95
|
+
set -g status-style bg=blue
|
|
96
|
+
|
|
97
|
+
windows:
|
|
98
|
+
- window_name: editor
|
|
99
|
+
panes:
|
|
100
|
+
- vim .
|
|
101
|
+
- window_name: shell
|
|
102
|
+
panes:
|
|
103
|
+
- ""
|
|
104
|
+
- window_name: logs
|
|
105
|
+
panes:
|
|
106
|
+
- tail -f /var/log/system.log
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Config directory
|
|
110
|
+
|
|
111
|
+
Each project gets its own `.$NAME.optmux.d/` directory:
|
|
112
|
+
|
|
113
|
+
| Path | Purpose |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `tmux/tmux.conf` | Main tmux config (editable after creation) |
|
|
116
|
+
| `tmux/tmux.*.conf` | Additional config files you can add |
|
|
117
|
+
| `tmux/tmux.sock` | Tmux server socket (isolates this project) |
|
|
118
|
+
| `tmux/plugins/` | TPM plugin directory |
|
|
119
|
+
| `tmux/plugins-update.sh` | Run manually to update all plugins |
|
|
120
|
+
|
|
121
|
+
## optmux YAML config
|
|
122
|
+
|
|
123
|
+
Add an `optmux:` section to your tmuxp YAML to configure shortcuts and tmux settings:
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
optmux:
|
|
127
|
+
shortcuts:
|
|
128
|
+
C-M-b: gh browse . # Ctrl-Alt-b: run command directly
|
|
129
|
+
C-M-e:
|
|
130
|
+
command: ${VISUAL:-${EDITOR:-vim}} README.md # exec directly (no shell)
|
|
131
|
+
window: true # open in a new-window
|
|
132
|
+
E:
|
|
133
|
+
send-keys: ${VISUAL:-${EDITOR:-vim}} . # send-keys (runs in a new shell)
|
|
134
|
+
zoom: false # do not zoom (default: true for splits)
|
|
135
|
+
tmux_config:
|
|
136
|
+
project-settings: |
|
|
137
|
+
set -g status-style bg=blue
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Shortcuts
|
|
141
|
+
|
|
142
|
+
Shortcuts bind tmux keys to commands:
|
|
143
|
+
|
|
144
|
+
- **`C-M-*` keys** are bound globally (no prefix needed)
|
|
145
|
+
- **Other keys** require the tmux prefix (`C-t`)
|
|
146
|
+
- **`command:`** executes directly (default for string values)
|
|
147
|
+
- **`send-keys:`** sends the command to a new shell (supports shell expansion)
|
|
148
|
+
- **`window: true`** opens in a new window instead of a split
|
|
149
|
+
- **`zoom: false`** disables auto-zoom on splits (default: true)
|
|
150
|
+
|
|
151
|
+
### tmux_config
|
|
152
|
+
|
|
153
|
+
Entries under `tmux_config:` are written as `tmux.optmux-extras.{name}.conf` files and auto-sourced by tmux.
|
|
154
|
+
|
|
155
|
+
### Personal config (`~/.optmux.yaml`)
|
|
156
|
+
|
|
157
|
+
Create `~/.optmux.yaml` to define personal defaults that apply to all optmux sessions:
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
optmux:
|
|
161
|
+
shortcuts:
|
|
162
|
+
C-M-g: lazygit
|
|
163
|
+
tmux_config:
|
|
164
|
+
my-defaults: |
|
|
165
|
+
set -g status-style bg=green
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Personal config is merged with per-project config. When both define the same key, **personal settings take precedence**.
|
|
169
|
+
|
|
170
|
+
### Customization
|
|
171
|
+
|
|
172
|
+
- Edit `tmux/tmux.conf` to change tmux settings
|
|
173
|
+
- Drop `tmux/tmux.mysetup.conf` files for additional config (auto-sourced)
|
|
174
|
+
- Run `tmux/plugins-update.sh` from inside tmux to update plugins
|
|
175
|
+
- Press `prefix + R` to reload the config
|
|
176
|
+
|
|
177
|
+
### Environment variables
|
|
178
|
+
|
|
179
|
+
optmux sets these before launching tmux/tmuxp:
|
|
180
|
+
|
|
181
|
+
| Variable | Value |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `OPTMUX_DIR` | Absolute path to the `.$NAME.optmux.d/` directory |
|
|
184
|
+
| `OPTMUX_NAME` | Name derived from YAML filename or cwd (e.g., `myproject`) |
|
|
185
|
+
| `TMUX_PLUGIN_MANAGER_PATH` | `$OPTMUX_DIR/tmux/plugins` |
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# install the latest main branch
|
|
191
|
+
uvx git+https://github.com/netj/optmux.git
|
|
192
|
+
|
|
193
|
+
# local editable install for development
|
|
194
|
+
uv tool install -e .
|
|
195
|
+
|
|
196
|
+
# test any local changes directly (best for testing branches)
|
|
197
|
+
uv run optmux ./example.optmux.yaml
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""optmux — tmuxp wrapper with per-workflow tmux config directories."""
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
def load_optmux_conf():
|
|
11
|
+
"""Load personal optmux config from ~/.optmux.yaml if it exists."""
|
|
12
|
+
conf_path = Path.home() / ".optmux.yaml"
|
|
13
|
+
if conf_path.exists():
|
|
14
|
+
with open(conf_path) as f:
|
|
15
|
+
data = yaml.safe_load(f) or {}
|
|
16
|
+
return data.get("optmux") or {}
|
|
17
|
+
return {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def merge_optmux(project, personal):
|
|
21
|
+
"""Merge project and personal optmux configs (personal wins)."""
|
|
22
|
+
merged = {}
|
|
23
|
+
for key in ("shortcuts", "tmux_config"):
|
|
24
|
+
proj = project.get(key) or {}
|
|
25
|
+
pers = personal.get(key) or {}
|
|
26
|
+
if proj or pers:
|
|
27
|
+
merged[key] = {**proj, **pers}
|
|
28
|
+
return merged
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_tmux_conf_files(tmux_dir, optmux):
|
|
32
|
+
"""Generate tmux conf files from merged optmux config."""
|
|
33
|
+
# clear all managed files first to avoid stale configs
|
|
34
|
+
for conf_file in tmux_dir.glob("tmux.optmux-*.conf"):
|
|
35
|
+
conf_file.unlink()
|
|
36
|
+
|
|
37
|
+
# optmux.shortcuts → tmux.optmux-shortcuts.conf
|
|
38
|
+
shortcuts = optmux.get("shortcuts") or {}
|
|
39
|
+
if shortcuts:
|
|
40
|
+
lines = []
|
|
41
|
+
for key, value in shortcuts.items():
|
|
42
|
+
bind = "bind -n" if key.startswith("C-M-") else "bind"
|
|
43
|
+
# normalize str to dict
|
|
44
|
+
if isinstance(value, str):
|
|
45
|
+
opts = {"command": value}
|
|
46
|
+
elif isinstance(value, dict):
|
|
47
|
+
opts = value
|
|
48
|
+
else:
|
|
49
|
+
continue
|
|
50
|
+
use_window = opts.get("new-window", False)
|
|
51
|
+
use_zoom = opts.get("zoom", True)
|
|
52
|
+
open_cmd = "new-window" if use_window else "split-window -v"
|
|
53
|
+
# build the tmux action
|
|
54
|
+
if "send-keys" in opts:
|
|
55
|
+
escaped = opts["send-keys"].replace("'", "'\\''")
|
|
56
|
+
action = f"{open_cmd} -c '#{{pane_current_path}}' \\; send-keys '{escaped}' Enter"
|
|
57
|
+
elif "command" in opts:
|
|
58
|
+
escaped = opts["command"].replace("'", "'\\''")
|
|
59
|
+
action = f"{open_cmd} -c '#{{pane_current_path}}' '{escaped}'"
|
|
60
|
+
else:
|
|
61
|
+
continue
|
|
62
|
+
if use_zoom and not use_window:
|
|
63
|
+
action += " \\; resize-pane -Z"
|
|
64
|
+
lines.append(f"{bind} {key} {action}\n")
|
|
65
|
+
(tmux_dir / "tmux.optmux-shortcuts.conf").write_text("".join(lines))
|
|
66
|
+
|
|
67
|
+
# optmux.tmux_config → tmux.optmux-extras.{name}.conf for each entry
|
|
68
|
+
tmux_config = optmux.get("tmux_config") or {}
|
|
69
|
+
for conf_name, content in tmux_config.items():
|
|
70
|
+
(tmux_dir / f"tmux.optmux-extras.{conf_name}.conf").write_text(content)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
if len(sys.argv) > 1:
|
|
75
|
+
# optmux NAME.optmux.yaml [TMUXP_ARGS...]
|
|
76
|
+
tmuxp_yaml = sys.argv[1]
|
|
77
|
+
remaining_args = sys.argv[2:]
|
|
78
|
+
|
|
79
|
+
yaml_path = Path(tmuxp_yaml).resolve()
|
|
80
|
+
yaml_dir = yaml_path.parent
|
|
81
|
+
|
|
82
|
+
# strip .yaml, then .tmuxp, then .optmux suffixes
|
|
83
|
+
name = yaml_path.stem
|
|
84
|
+
for suffix in (".tmuxp", ".optmux", ".optmuxp"):
|
|
85
|
+
if name.endswith(suffix):
|
|
86
|
+
name = name[: -len(suffix)]
|
|
87
|
+
|
|
88
|
+
optmux_dir = yaml_dir / f".{name}.optmux.d"
|
|
89
|
+
else:
|
|
90
|
+
# optmux (no args) — just open tmux in cwd
|
|
91
|
+
name = Path.cwd().name
|
|
92
|
+
optmux_dir = Path.cwd() / ".optmux.d"
|
|
93
|
+
|
|
94
|
+
tmux_dir = optmux_dir / "tmux"
|
|
95
|
+
tmux_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
|
|
97
|
+
# seed bundled files if not present
|
|
98
|
+
bundled = files("optmux").joinpath("data")
|
|
99
|
+
tmux_conf = tmux_dir / "tmux.conf"
|
|
100
|
+
shutil.copy2(bundled / "tmux.conf", tmux_conf) # always regenerated; use tmux.*.conf for customizations
|
|
101
|
+
setup_script = tmux_dir / "plugins-update.sh"
|
|
102
|
+
if not setup_script.exists():
|
|
103
|
+
shutil.copy2(bundled / "plugins-update.sh", setup_script)
|
|
104
|
+
setup_script.chmod(0o755)
|
|
105
|
+
tips_script = tmux_dir / "tips.sh"
|
|
106
|
+
if not tips_script.exists():
|
|
107
|
+
shutil.copy2(bundled / "tips.sh", tips_script)
|
|
108
|
+
tips_script.chmod(0o755)
|
|
109
|
+
|
|
110
|
+
# generate tmux conf files from optmux YAML merged with personal config
|
|
111
|
+
personal = load_optmux_conf()
|
|
112
|
+
if len(sys.argv) > 1:
|
|
113
|
+
with open(yaml_path) as f:
|
|
114
|
+
data = yaml.safe_load(f) or {}
|
|
115
|
+
project = data.get("optmux") or {}
|
|
116
|
+
optmux = merge_optmux(project, personal)
|
|
117
|
+
else:
|
|
118
|
+
optmux = personal
|
|
119
|
+
generate_tmux_conf_files(tmux_dir, optmux)
|
|
120
|
+
|
|
121
|
+
# ensure scripts from optmux's own venv (e.g., tmuxp) are on PATH
|
|
122
|
+
venv_bin = str(Path(sys.executable).parent)
|
|
123
|
+
os.environ["PATH"] = venv_bin + os.pathsep + os.environ.get("PATH", "")
|
|
124
|
+
|
|
125
|
+
os.environ["OPTMUX_DIR"] = str(optmux_dir)
|
|
126
|
+
os.environ["OPTMUX_NAME"] = name
|
|
127
|
+
os.environ["TMUX_PLUGIN_MANAGER_PATH"] = str(tmux_dir / "plugins")
|
|
128
|
+
|
|
129
|
+
# bootstrap TPM (clone only); plugin install happens inside tmux via tmux.conf
|
|
130
|
+
subprocess.run([str(setup_script)], check=True)
|
|
131
|
+
|
|
132
|
+
sock = str(tmux_dir / "tmux.sock")
|
|
133
|
+
conf = str(tmux_conf)
|
|
134
|
+
|
|
135
|
+
if len(sys.argv) > 1:
|
|
136
|
+
os.execvp(
|
|
137
|
+
"tmuxp",
|
|
138
|
+
["tmuxp", "load", "--yes", "-S", sock, "-f", conf, tmuxp_yaml, *remaining_args],
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
# attach to existing session on this socket, or create a new one
|
|
142
|
+
has_session = subprocess.run(
|
|
143
|
+
["tmux", "-S", sock, "has-session"],
|
|
144
|
+
capture_output=True,
|
|
145
|
+
).returncode == 0
|
|
146
|
+
if has_session:
|
|
147
|
+
os.execvp("tmux", ["tmux", "-S", sock, "attach-session"])
|
|
148
|
+
else:
|
|
149
|
+
os.execvp("tmux", ["tmux", "-S", sock, "-f", conf, "new-session", "-s", f"optmux {name}"])
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# optmux tmux plugin setup/update script
|
|
3
|
+
# Bootstraps TPM, installs missing plugins, and updates all plugins.
|
|
4
|
+
# Run manually to update: ./path/to/workflow.optmux.d/tmux/plugins-update.sh
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
: ${OPTMUX_DIR:="$(cd "$(dirname "$0")/.."; pwd)"}
|
|
8
|
+
: ${TMUX_PLUGIN_MANAGER_PATH:="$OPTMUX_DIR/tmux/plugins"}
|
|
9
|
+
export TMUX_PLUGIN_MANAGER_PATH
|
|
10
|
+
export XDG_CONFIG_HOME="$OPTMUX_DIR"
|
|
11
|
+
|
|
12
|
+
tpm=netj/tpm # XXX using netj/tpm fork; TODO switch back to tmux-plugins/tpm
|
|
13
|
+
|
|
14
|
+
if [[ ! -x "$TMUX_PLUGIN_MANAGER_PATH"/$tpm/tpm ]]; then
|
|
15
|
+
echo "optmux: installing TPM ($tpm)..."
|
|
16
|
+
git clone https://github.com/$tpm "$TMUX_PLUGIN_MANAGER_PATH"/$tpm
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if [[ -n "${TMUX:-}" ]]; then
|
|
20
|
+
echo "optmux: installing/updating tmux plugins..."
|
|
21
|
+
"$TMUX_PLUGIN_MANAGER_PATH"/$tpm/bin/install_plugins
|
|
22
|
+
"$TMUX_PLUGIN_MANAGER_PATH"/$tpm/bin/update_plugins all
|
|
23
|
+
# reload config to activate newly installed/updated plugins
|
|
24
|
+
tmux source-file "$OPTMUX_DIR/tmux/tmux.conf"
|
|
25
|
+
fi
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# optmux tips — shows key binding cheatsheet and hints
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
: ${OPTMUX_DIR:="$(cd "$(dirname "$0")/.."; pwd)"}
|
|
6
|
+
dismissed="$OPTMUX_DIR/tmux/.tips-dismissed"
|
|
7
|
+
|
|
8
|
+
# check suppression
|
|
9
|
+
if [[ -e "$dismissed" ]]; then
|
|
10
|
+
if grep -q '^forever$' "$dismissed" 2>/dev/null; then
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
# skip if dismissed less than 7 days ago
|
|
14
|
+
if find "$dismissed" -mtime -7 -print -quit 2>/dev/null | grep -q .; then
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# nerd font hint
|
|
20
|
+
nerd_font_tip=""
|
|
21
|
+
if ! fc-list : family 2>/dev/null | grep -qi 'Nerd Font'; then
|
|
22
|
+
nerd_font_tip="
|
|
23
|
+
[!] Install a Nerd Font for best experience
|
|
24
|
+
https://www.nerdfonts.com"
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
clear
|
|
28
|
+
cat <<EOF
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
optmux tips
|
|
32
|
+
|
|
33
|
+
Prefix: Ctrl+T (C-t)
|
|
34
|
+
|
|
35
|
+
Workflow: C-M-c wtcode or C-M-f find file to open editor
|
|
36
|
+
-> C-M-g lazygit to check diff/commits
|
|
37
|
+
-> C-M-o cycle between panes or q to return
|
|
38
|
+
-> C-M-s shell in same dir (run tests, one-off commands)
|
|
39
|
+
|
|
40
|
+
Install: brew install netj/tap/wtcode https://github.com/netj/wtcode
|
|
41
|
+
brew install lazygit https://github.com/jesseduffield/lazygit
|
|
42
|
+
|
|
43
|
+
C-t C-t last window C-M-h/j/k/l navigate panes
|
|
44
|
+
C-t C-c new window C-M-z quick toggle zoom
|
|
45
|
+
C-t C-n/p next/prev window C-M-\\ last pane
|
|
46
|
+
C-t n/p next/prev w/ bell C-M-o prev pane + zoom
|
|
47
|
+
C-t z toggle zoom
|
|
48
|
+
C-t o cycle panes
|
|
49
|
+
C-t R reload config
|
|
50
|
+
|
|
51
|
+
C-t t send prefix to nested tmux
|
|
52
|
+
C-t T swap prefix (for nested tmux)
|
|
53
|
+
copy-mode yank auto-copies to system clipboard (OSC 52)
|
|
54
|
+
${nerd_font_tip}
|
|
55
|
+
|
|
56
|
+
q/Enter: dismiss d: dismiss for a week D: dismiss forever
|
|
57
|
+
|
|
58
|
+
EOF
|
|
59
|
+
|
|
60
|
+
# wait for user input
|
|
61
|
+
while true; do
|
|
62
|
+
read -rsn1 key
|
|
63
|
+
case "$key" in
|
|
64
|
+
q|"")
|
|
65
|
+
break
|
|
66
|
+
;;
|
|
67
|
+
d)
|
|
68
|
+
touch "$dismissed"
|
|
69
|
+
break
|
|
70
|
+
;;
|
|
71
|
+
D)
|
|
72
|
+
echo "forever" > "$dismissed"
|
|
73
|
+
break
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
done
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# optmux provided "optimal" TMUX configuration for most workflows
|
|
2
|
+
# Origin: https://github.com/netj/dotfiles/blob/main/.tmux.conf
|
|
3
|
+
|
|
4
|
+
###############################################################################
|
|
5
|
+
# Essential config
|
|
6
|
+
###############################################################################
|
|
7
|
+
# C-t as prefix not C-b b/c it's TMUX not BMUX; moreover, C-b/C-f is essential for EMACS-mode readline in BASH/etc.
|
|
8
|
+
set -g prefix C-t; unbind C-b
|
|
9
|
+
set -g default-command '$SHELL -l' # ensure starting a login shell
|
|
10
|
+
|
|
11
|
+
set -g base-index 1
|
|
12
|
+
set -g pane-base-index 1
|
|
13
|
+
|
|
14
|
+
set -g display-time 2500
|
|
15
|
+
setw -g clock-mode-style 24
|
|
16
|
+
setw -g wrap-search off
|
|
17
|
+
setw -g mouse on
|
|
18
|
+
set -g set-clipboard on # emit OSC 52 on yank so terminal sets system clipboard
|
|
19
|
+
set -g allow-passthrough on # let nested/inner programs' OSC 52 reach outer terminal (tmux 3.3+)
|
|
20
|
+
setw -g monitor-activity on
|
|
21
|
+
set -g set-titles on
|
|
22
|
+
|
|
23
|
+
set -g set-titles-string "#T - #I:#W#F#S^#h"
|
|
24
|
+
set -g status-left "#T"
|
|
25
|
+
set -g status-left-length 48
|
|
26
|
+
set -g status-right "#[fg=white,bright]#S#[default]^#[fg=white]#h#[default]"
|
|
27
|
+
set -g status-right-length 16
|
|
28
|
+
set -g status-style bg=black
|
|
29
|
+
set -ga status-style fg=brightblack
|
|
30
|
+
set -g status-left-style fg=green,bright
|
|
31
|
+
setw -g window-status-style fg=brightblack
|
|
32
|
+
setw -g window-status-current-style fg=green,bright
|
|
33
|
+
setw -g window-status-activity-style fg=yellow,bright
|
|
34
|
+
setw -g window-status-bell-style fg=red,bright
|
|
35
|
+
setw -g window-status-bell-style fg=red,bright,blink
|
|
36
|
+
setw -g window-status-format " #I:#{b:pane_current_path}#F "
|
|
37
|
+
setw -g window-status-current-format " #I:#{b:pane_current_path}#F "
|
|
38
|
+
|
|
39
|
+
set -g pane-border-status top
|
|
40
|
+
set -g pane-border-format " #P: #{pane_current_command} | #T "
|
|
41
|
+
|
|
42
|
+
###############################################################################
|
|
43
|
+
# TPM Plugins
|
|
44
|
+
###############################################################################
|
|
45
|
+
set -g @plugin 'tmux-plugins/tmux-sensible' # hx limit, mouse, C-n/C-p over awkward n/p
|
|
46
|
+
|
|
47
|
+
# vim-tmux-navigator with C-M-h/j/k/l (preserving custom bindings)
|
|
48
|
+
set -g @plugin 'tmux-plugins/tmux-pain-control' # move around panes with j and k, a bit like vim; resize panes like vim
|
|
49
|
+
set -g @plugin 'christoomey/vim-tmux-navigator'
|
|
50
|
+
set -g @vim_navigator_mapping_left "C-M-h"
|
|
51
|
+
set -g @vim_navigator_mapping_down "C-M-j"
|
|
52
|
+
set -g @vim_navigator_mapping_up "C-M-k"
|
|
53
|
+
set -g @vim_navigator_mapping_right "C-M-l"
|
|
54
|
+
set -g @vim_navigator_mapping_prev ""
|
|
55
|
+
set -g @vim_navigator_prefix_mapping_clear_screen ""
|
|
56
|
+
|
|
57
|
+
set -g @plugin 'tmux-plugins/tmux-copycat'
|
|
58
|
+
set -g @plugin 'tmux-plugins/tmux-yank'
|
|
59
|
+
set -g @plugin 'nhdaly/tmux-better-mouse-mode'
|
|
60
|
+
|
|
61
|
+
# resurrect
|
|
62
|
+
set -g @plugin 'tmux-plugins/tmux-resurrect'
|
|
63
|
+
set -g @resurrect-strategy-vim 'session'
|
|
64
|
+
set -g @resurrect-capture-pane-contents 'on'
|
|
65
|
+
|
|
66
|
+
# continuum
|
|
67
|
+
#set -g @plugin 'tmux-plugins/tmux-continuum'
|
|
68
|
+
#set -g @continuum-restore 'on'
|
|
69
|
+
|
|
70
|
+
set -g @plugin 'netj/claritmux'
|
|
71
|
+
set -g @claritmux_status_workdir 0
|
|
72
|
+
set -g @claritmux_status_date 0
|
|
73
|
+
set -g @claritmux_status_time 0
|
|
74
|
+
set -g @claritmux_status_hostname 1
|
|
75
|
+
|
|
76
|
+
## Initialize TPM (bootstrap is handled by tmux/plugins-update.sh before tmux starts)
|
|
77
|
+
set -g @tpm 'netj/tpm' # XXX using my fork netj/tpm for now; TODO switch back to tmux-plugins/tpm once merging outstanding improvements
|
|
78
|
+
set -gF @plugin '#{@tpm}'
|
|
79
|
+
set-environment -g OPTMUX_DIR "$OPTMUX_DIR"
|
|
80
|
+
set-environment -g TMUX_PLUGIN_MANAGER_PATH "$OPTMUX_DIR/tmux/plugins"
|
|
81
|
+
run 'export XDG_CONFIG_HOME="$OPTMUX_DIR"; tpm=$(tmux show -gv @tpm); "$TMUX_PLUGIN_MANAGER_PATH"/$tpm/tpm'
|
|
82
|
+
# Window 0: plugins update + tips pane
|
|
83
|
+
set-hook -g session-created 'new-window -t 0 -n "optmux" "$OPTMUX_DIR/tmux/tips.sh" ; split-window -t 0 -v "$OPTMUX_DIR/tmux/plugins-update.sh"'
|
|
84
|
+
|
|
85
|
+
###############################################################################
|
|
86
|
+
# Essential config, continued; also compensating plugin
|
|
87
|
+
###############################################################################
|
|
88
|
+
|
|
89
|
+
## reload optmux config (overrides tmux-sensible's R which reloads ~/.tmux.conf)
|
|
90
|
+
bind R source-file "$OPTMUX_DIR/tmux/tmux.conf" \; display-message "optmux: reloaded $OPTMUX_DIR/tmux/tmux.conf"
|
|
91
|
+
|
|
92
|
+
## easier session/window actions
|
|
93
|
+
unbind ^D; bind ^D detach # easier detach w/ Ctrl
|
|
94
|
+
unbind ^C; bind ^C new-window -c "#{pane_current_path}" # easier new window
|
|
95
|
+
unbind C; bind C new-session # easier new session
|
|
96
|
+
bind Tab attach-session -d # easier to detach all other clients to make this client the only one
|
|
97
|
+
bind ` confirm-before kill-session # easier kill-session
|
|
98
|
+
|
|
99
|
+
## settings for TMUX in TMUX windows/panes
|
|
100
|
+
# C-t t to send prefix is a more sensible escaping scheme than TMUX or tmux-plugins/tmux-sensible's default
|
|
101
|
+
# b/c when using TMUX in TMUX (double-decker or more), you really don't want to press C-t 2^k times but just press C-t once followed by the t key only k times for sending the prefix key to the k-th inner/nested TMUX sessions
|
|
102
|
+
unbind C-t; bind C-t last-window
|
|
103
|
+
unbind t; bind t send-prefix
|
|
104
|
+
# swap outer TMUX prefix to a different one to let the usual prefix directly reach the inner TMUX
|
|
105
|
+
bind T \
|
|
106
|
+
if-shell "[ \"`tmux show-options prefix`\" = 'prefix C-t' ]" \
|
|
107
|
+
"set prefix C-s; unbind -n M-Up; unbind -n M-PageUp" \
|
|
108
|
+
"set prefix C-t; bind -n M-Up copy-mode; bind -n M-PageUp copy-mode" \
|
|
109
|
+
#
|
|
110
|
+
|
|
111
|
+
## easier window selection with an alert ("bell") via lifting Ctrl (NOTE prefix C-n/C-p in tmux-sensible selects all windows)
|
|
112
|
+
bind n run-shell 'wc=#{window_index} ws=$(tmux list-windows -F "##{?window_bell_flag, ,@}##{window_index}"); wb=$({ echo "$ws"; echo "$ws"; } | grep -A9999 ".$wc\$" | tail -n +2 | grep "^ " | head -1); if [[ -n $wb ]]; then tmux select-window -t $wb; else tmux next-window; fi'
|
|
113
|
+
bind p run-shell 'wc=#{window_index} ws=$(tmux list-windows -F "##{?window_bell_flag, ,@}##{window_index}"); wb=$({ echo "$ws"; echo "$ws"; } | grep -B9999 ".$wc\$" | head -n -1 | grep "^ " | tail -1); if [[ -n $wb ]]; then tmux select-window -t $wb; else tmux previous-window; fi'
|
|
114
|
+
|
|
115
|
+
## easier pane selection/rotation (default is not symmetric and a bit awkward)
|
|
116
|
+
bind C-o select-pane -t :.+
|
|
117
|
+
bind o select-pane -t :.-
|
|
118
|
+
bind O rotate-window
|
|
119
|
+
bind M-o rotate-window -D
|
|
120
|
+
|
|
121
|
+
## Ctrl+Opt/Alt: quicker access to frequently used prefix combos
|
|
122
|
+
bind -n C-M-z resize-pane -Z # quick toggle zoom pane (normally prefix z)
|
|
123
|
+
bind -n C-M-\\ select-pane -l \; resize-pane -Z # quick switching btwn last panes (normally prefix ;) cf. vim_navigator_mapping_prev
|
|
124
|
+
bind -n C-M-o select-pane -t :.-\; resize-pane -Z # switch to prev pane and also zoom
|
|
125
|
+
# quicker split
|
|
126
|
+
bind -n C-M-s split-window -v -c "#{pane_current_path}" \; resize-pane -Z
|
|
127
|
+
|
|
128
|
+
# quick commands in new pane with Ctrl+Opt
|
|
129
|
+
bind -n C-M-c split-window -v -c "#{pane_current_path}" wtcode \; resize-pane -Z
|
|
130
|
+
bind -n C-M-f split-window -v -c "#{pane_current_path}" '${VISUAL:-${EDITOR:-vim}} $(fzf || echo .)' \; resize-pane -Z
|
|
131
|
+
bind -n C-M-g split-window -v -c "#{pane_current_path}" lazygit \; resize-pane -Z
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
###############################################################################
|
|
135
|
+
# Local config
|
|
136
|
+
###############################################################################
|
|
137
|
+
# Source all tmux.*.conf files from the optmux dir
|
|
138
|
+
if-shell "ls \"$OPTMUX_DIR\"/tmux/tmux.*.conf >/dev/null 2>&1" \
|
|
139
|
+
"source-file $OPTMUX_DIR/tmux/tmux.*.conf"
|
|
140
|
+
|
|
141
|
+
# vim:ft=tmux
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "optmux"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Optimal, opinionated, batteries-included TMUX that's neat and easy for any project"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Jaeho Shin", email = "netj@sparcs.org" }
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"tmuxp",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
optmux = "optmux.cli:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.10.10,<0.11.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[tool.uv.build-backend]
|
|
23
|
+
module-root = ""
|