betterterm 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # betterterm
2
+
3
+ Terminal IDE setup: Ghostty + tmux + yazi + carbonyl browser.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx betterterm
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ - **macOS** with [Homebrew](https://brew.sh)
14
+ - **[Ghostty](https://ghostty.org)** terminal emulator
15
+
16
+ The installer handles everything else (tmux, yazi, carbonyl).
17
+
18
+ ## What it does
19
+
20
+ - Installs tmux, yazi, and carbonyl via Homebrew/npm
21
+ - Configures tmux with a clean status bar, mouse support, and keybindings
22
+ - Sets up yazi as a toggleable file explorer with cd-to-terminal sync
23
+ - Adds an in-terminal browser (carbonyl) toggled via hotkey
24
+ - Patches your Ghostty config with Cmd-key shortcuts that map to tmux
25
+
26
+ ## Keybindings
27
+
28
+ | Key | Action |
29
+ |-----|--------|
30
+ | `Cmd+E` | Toggle file explorer (yazi) |
31
+ | `Cmd+Y` | Toggle browser (carbonyl) |
32
+ | `Cmd+Shift+Y` | Close browser |
33
+ | `Cmd+L` | Browser URL bar |
34
+ | `Cmd+T` | New tmux window |
35
+ | `Cmd+]` | Next window |
36
+ | `Cmd+[` | Previous window |
37
+ | `Cmd+\` | Split pane vertically |
38
+ | `Cmd+-` | Split pane horizontally |
39
+ | `Cmd+Arrow` | Resize pane |
40
+ | `Cmd+R` | Reload tmux config |
41
+ | `Ctrl+B, Tab` | Toggle between panes |
42
+
43
+ ## Uninstall
44
+
45
+ The installer backs up any existing configs to `*.backup` files. To revert:
46
+
47
+ ```bash
48
+ # Restore configs
49
+ mv ~/.tmux.conf.backup ~/.tmux.conf
50
+ mv ~/.config/yazi/yazi.toml.backup ~/.config/yazi/yazi.toml
51
+ mv ~/.config/yazi/keymap.toml.backup ~/.config/yazi/keymap.toml
52
+
53
+ # Remove betterterm block from Ghostty config (between the marker comments)
54
+ # Remove scripts
55
+ rm ~/.local/bin/betterterm*.sh ~/.local/bin/toggle-*.sh ~/.local/bin/open-browser.sh ~/.local/bin/close-browser.sh ~/.local/bin/browser-nav.sh
56
+ ```
57
+
58
+ ## License
59
+
60
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync, spawnSync } = require("child_process");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const os = require("os");
7
+
8
+ const HOME = os.homedir();
9
+ const PKG_ROOT = path.resolve(__dirname, "..");
10
+
11
+ // ── Helpers ──────────────────────────────────────────────
12
+
13
+ function run(cmd, opts = {}) {
14
+ try {
15
+ return execSync(cmd, { encoding: "utf-8", stdio: "pipe", ...opts }).trim();
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ function which(bin) {
22
+ return run(`which ${bin}`);
23
+ }
24
+
25
+ function log(msg) {
26
+ console.log(` \x1b[32m✓\x1b[0m ${msg}`);
27
+ }
28
+
29
+ function warn(msg) {
30
+ console.log(` \x1b[33m⚠\x1b[0m ${msg}`);
31
+ }
32
+
33
+ function err(msg) {
34
+ console.error(` \x1b[31m✗\x1b[0m ${msg}`);
35
+ }
36
+
37
+ function heading(msg) {
38
+ console.log(`\n\x1b[1m${msg}\x1b[0m`);
39
+ }
40
+
41
+ function backup(filePath) {
42
+ if (fs.existsSync(filePath)) {
43
+ const content = fs.readFileSync(filePath, "utf-8");
44
+ if (content.includes("betterterm")) {
45
+ return false; // Already a betterterm config, no backup needed
46
+ }
47
+ const backupPath = filePath + ".backup";
48
+ fs.copyFileSync(filePath, backupPath);
49
+ warn(`Backed up existing ${filePath} → ${backupPath}`);
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function ensureDir(dir) {
56
+ fs.mkdirSync(dir, { recursive: true });
57
+ }
58
+
59
+ // ── 1. Check for Homebrew ────────────────────────────────
60
+
61
+ heading("betterterm setup");
62
+
63
+ if (!which("brew")) {
64
+ err("Homebrew is required but not installed.");
65
+ console.log(" Install it: https://brew.sh");
66
+ process.exit(1);
67
+ }
68
+ log("Homebrew found");
69
+
70
+ // ── 2. Install brew dependencies ─────────────────────────
71
+
72
+ heading("Installing dependencies...");
73
+
74
+ const brewDeps = ["tmux", "yazi"];
75
+ for (const dep of brewDeps) {
76
+ if (which(dep)) {
77
+ log(`${dep} already installed`);
78
+ } else {
79
+ console.log(` Installing ${dep}...`);
80
+ const result = spawnSync("brew", ["install", dep], { stdio: "inherit" });
81
+ if (result.status !== 0) {
82
+ err(`Failed to install ${dep}`);
83
+ process.exit(1);
84
+ }
85
+ log(`${dep} installed`);
86
+ }
87
+ }
88
+
89
+ // ── 3. Install carbonyl ──────────────────────────────────
90
+
91
+ const carbonylDir = path.join(HOME, ".local/lib/node_modules/carbonyl");
92
+ if (fs.existsSync(carbonylDir)) {
93
+ log("carbonyl already installed");
94
+ } else {
95
+ console.log(" Installing carbonyl (this may take a moment)...");
96
+ const result = spawnSync(
97
+ "npm",
98
+ ["install", "--global", "carbonyl", "--prefix", path.join(HOME, ".local")],
99
+ { stdio: "inherit" }
100
+ );
101
+ if (result.status !== 0) {
102
+ warn("carbonyl installation failed — browser features won't work");
103
+ } else {
104
+ log("carbonyl installed");
105
+ }
106
+ }
107
+
108
+ // ── 4. Copy scripts to ~/.local/bin/ ─────────────────────
109
+
110
+ heading("Installing scripts...");
111
+
112
+ const binDir = path.join(HOME, ".local/bin");
113
+ ensureDir(binDir);
114
+
115
+ const scripts = fs.readdirSync(path.join(PKG_ROOT, "scripts"));
116
+ for (const script of scripts) {
117
+ const src = path.join(PKG_ROOT, "scripts", script);
118
+ const dest = path.join(binDir, script);
119
+ fs.copyFileSync(src, dest);
120
+ fs.chmodSync(dest, 0o755);
121
+ log(`${script} → ${dest}`);
122
+ }
123
+
124
+ // ── 5. Copy configs ──────────────────────────────────────
125
+
126
+ heading("Installing configs...");
127
+
128
+ // tmux.conf
129
+ const tmuxDest = path.join(HOME, ".tmux.conf");
130
+ backup(tmuxDest);
131
+ fs.copyFileSync(path.join(PKG_ROOT, "configs/tmux.conf"), tmuxDest);
132
+ log(`tmux.conf → ${tmuxDest}`);
133
+
134
+ // yazi configs
135
+ const yaziDir = path.join(HOME, ".config/yazi");
136
+ ensureDir(yaziDir);
137
+
138
+ const yaziTomlDest = path.join(yaziDir, "yazi.toml");
139
+ backup(yaziTomlDest);
140
+ fs.copyFileSync(path.join(PKG_ROOT, "configs/yazi.toml"), yaziTomlDest);
141
+ log(`yazi.toml → ${yaziTomlDest}`);
142
+
143
+ const keymapDest = path.join(yaziDir, "keymap.toml");
144
+ backup(keymapDest);
145
+ fs.copyFileSync(path.join(PKG_ROOT, "configs/keymap.toml"), keymapDest);
146
+ log(`keymap.toml → ${keymapDest}`);
147
+
148
+ // yazi plugin
149
+ const pluginDir = path.join(yaziDir, "plugins/cd-terminal.yazi");
150
+ ensureDir(pluginDir);
151
+ fs.copyFileSync(
152
+ path.join(PKG_ROOT, "plugins/cd-terminal.yazi/init.lua"),
153
+ path.join(pluginDir, "init.lua")
154
+ );
155
+ log(`cd-terminal.yazi plugin → ${pluginDir}/init.lua`);
156
+
157
+ // ── 6. Patch Ghostty config ─────────────────────────────
158
+
159
+ heading("Configuring Ghostty...");
160
+
161
+ const ghosttyDir = path.join(HOME, ".config/ghostty");
162
+ const ghosttyConfig = path.join(ghosttyDir, "config");
163
+ const ghosttyBlock = fs.readFileSync(
164
+ path.join(PKG_ROOT, "configs/ghostty.conf"),
165
+ "utf-8"
166
+ );
167
+
168
+ ensureDir(ghosttyDir);
169
+
170
+ if (fs.existsSync(ghosttyConfig)) {
171
+ let content = fs.readFileSync(ghosttyConfig, "utf-8");
172
+ const startMarker = "# --- betterterm start ---";
173
+ const endMarker = "# --- betterterm end ---";
174
+ const startIdx = content.indexOf(startMarker);
175
+ const endIdx = content.indexOf(endMarker);
176
+
177
+ if (startIdx !== -1 && endIdx !== -1) {
178
+ // Replace existing block
179
+ content =
180
+ content.substring(0, startIdx) +
181
+ ghosttyBlock.trimEnd() +
182
+ "\n" +
183
+ content.substring(endIdx + endMarker.length + 1);
184
+ fs.writeFileSync(ghosttyConfig, content);
185
+ log("Updated existing betterterm block in Ghostty config");
186
+ } else {
187
+ // Append block
188
+ const separator = content.endsWith("\n") ? "\n" : "\n\n";
189
+ fs.appendFileSync(ghosttyConfig, separator + ghosttyBlock);
190
+ log("Appended betterterm block to Ghostty config");
191
+ }
192
+ } else {
193
+ fs.writeFileSync(ghosttyConfig, ghosttyBlock);
194
+ log("Created Ghostty config with betterterm block");
195
+ }
196
+
197
+ // ── 7. Check PATH ────────────────────────────────────────
198
+
199
+ const pathDirs = (process.env.PATH || "").split(":");
200
+ if (!pathDirs.includes(binDir)) {
201
+ warn(`${binDir} is not in your PATH`);
202
+ console.log(` Add this to your shell profile (~/.zshrc or ~/.bashrc):`);
203
+ console.log(` export PATH="$HOME/.local/bin:$PATH"`);
204
+ }
205
+
206
+ // ── 8. Summary ───────────────────────────────────────────
207
+
208
+ heading("Setup complete!");
209
+ console.log(`
210
+ Restart Ghostty to activate betterterm.
211
+
212
+ \x1b[1mKeybindings:\x1b[0m
213
+ Cmd+E Toggle file explorer (yazi)
214
+ Cmd+Y Toggle browser (carbonyl)
215
+ Cmd+Shift+Y Close browser
216
+ Cmd+L Browser URL bar
217
+ Cmd+T New tmux window
218
+ Cmd+] Next window
219
+ Cmd+[ Previous window
220
+ Cmd+\\ Split pane vertically
221
+ Cmd+- Split pane horizontally
222
+ Cmd+Arrow Resize pane
223
+ Cmd+R Reload tmux config
224
+ Ctrl+B, Tab Toggle between panes
225
+ Opt+Enter Send Enter to terminal
226
+ `);
@@ -0,0 +1,24 @@
1
+ # --- betterterm start ---
2
+
3
+ # Launch tmux with betterterm
4
+ command = ~/.local/bin/betterterm.sh
5
+
6
+ # Cmd shortcuts → tmux (sends Ctrl+B prefix then the key)
7
+ keybind = cmd+e=text:\x02e
8
+ keybind = cmd+shift+tab=text:\x02\t
9
+ keybind = cmd+up=text:\x02\x1b[A
10
+ keybind = cmd+down=text:\x02\x1b[B
11
+ keybind = cmd+left=text:\x02\x1b[D
12
+ keybind = cmd+right=text:\x02\x1b[C
13
+ keybind = cmd+backslash=text:\x02|
14
+ keybind = cmd+-=text:\x02-
15
+ keybind = cmd+r=text:\x02r
16
+ keybind = cmd+y=text:\x02b
17
+ keybind = cmd+shift+y=text:\x02B
18
+ keybind = cmd+l=text:\x02l
19
+ keybind = opt+enter=text:\x0d
20
+ keybind = cmd+t=text:\x02c
21
+ keybind = cmd+]=text:\x02n
22
+ keybind = cmd+[=text:\x02p
23
+
24
+ # --- betterterm end ---
@@ -0,0 +1,4 @@
1
+ [[mgr.prepend_keymap]]
2
+ on = ["<Enter>"]
3
+ run = ["enter", "plugin cd-terminal"]
4
+ desc = "Enter directory and cd terminal there"
@@ -0,0 +1,62 @@
1
+ # --- betterterm tmux config ---
2
+
3
+ # Terminal compatibility (fixes yazi terminal response timeout)
4
+ set -g default-terminal "tmux-256color"
5
+ set -ga terminal-overrides ",xterm-ghostty:RGB,xterm-256color:RGB"
6
+ set -g allow-passthrough on
7
+ set -g escape-time 0
8
+ set -ga update-environment "TERM TERM_PROGRAM TERM_PROGRAM_VERSION"
9
+
10
+ # Mouse support (resize panes by dragging borders, click to focus, scroll)
11
+ set -g mouse on
12
+
13
+ # Start window/pane numbering at 1
14
+ set -g base-index 1
15
+ setw -g pane-base-index 1
16
+
17
+ # Quick toggle: jump between terminal and yazi panes
18
+ bind Tab select-pane -t :.+
19
+
20
+ # Toggle yazi file explorer (Ctrl+B then e)
21
+ bind e run-shell "~/.local/bin/toggle-yazi.sh"
22
+
23
+ # Toggle browser (Ctrl+B then b) / Close browser (Ctrl+B then B)
24
+ bind b run-shell "~/.local/bin/toggle-browser.sh"
25
+ bind B run-shell "~/.local/bin/close-browser.sh"
26
+ bind l run-shell "~/.local/bin/browser-nav.sh"
27
+
28
+ # Resize panes with prefix + arrow keys (5 cells at a time)
29
+ bind -r Up resize-pane -U 5
30
+ bind -r Down resize-pane -D 5
31
+ bind -r Left resize-pane -L 5
32
+ bind -r Right resize-pane -R 5
33
+
34
+ # Reload config with prefix + r
35
+ bind r source-file ~/.tmux.conf \; display "Config reloaded"
36
+
37
+ # Better split keybindings
38
+ bind | split-window -h -c "#{pane_current_path}"
39
+ bind - split-window -v -c "#{pane_current_path}"
40
+
41
+ # Don't rename windows automatically
42
+ set -g allow-rename off
43
+
44
+ # --- Appearance ---
45
+ set -g status-position bottom
46
+ set -g status-style "bg=#1a1a1a,fg=#888888"
47
+
48
+ # Left: status buttons (visual indicators + hotkey reminders)
49
+ set -g status-left-length 50
50
+ set -g status-left "#[fg=#888888,bg=#2a2a2a] Files:Cmd+E #[default] #[fg=#888888,bg=#2a2a2a] Browser:Cmd+Y #[default] "
51
+
52
+ # Window list
53
+ setw -g window-status-format "#[fg=#555555]#I:#W"
54
+ setw -g window-status-current-format "#[fg=#cccccc,bold]#I:#W"
55
+
56
+ # Right: system stats + time
57
+ set -g status-right-length 60
58
+ set -g status-right "#[fg=#555555]#(~/.local/bin/betterterm-stats.sh) #[fg=#888888]%H:%M"
59
+ set -g status-interval 5
60
+
61
+ set -g pane-border-style "fg=#333333"
62
+ set -g pane-active-border-style "fg=#888888"
@@ -0,0 +1,11 @@
1
+ [mgr]
2
+ ratio = [1, 3, 3]
3
+
4
+ [preview]
5
+ image_filter = "triangle"
6
+ image_quality = 75
7
+
8
+ [opener]
9
+
10
+ [plugin]
11
+ preload_image = false
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "betterterm",
3
+ "version": "1.0.0",
4
+ "description": "Terminal IDE setup: Ghostty + tmux + yazi + carbonyl browser",
5
+ "bin": {
6
+ "betterterm": "bin/cli.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "configs/",
11
+ "scripts/",
12
+ "plugins/"
13
+ ],
14
+ "keywords": [
15
+ "terminal",
16
+ "tmux",
17
+ "yazi",
18
+ "ghostty",
19
+ "ide",
20
+ "cli"
21
+ ],
22
+ "author": "Myles",
23
+ "license": "MIT"
24
+ }
@@ -0,0 +1,11 @@
1
+ --- cd-terminal plugin: sends cd command to the left tmux pane
2
+ return {
3
+ entry = function()
4
+ local dir = cx.active.current.cwd
5
+ local cmd = string.format(
6
+ "tmux send-keys -t 1 'cd %q' Enter",
7
+ tostring(dir)
8
+ )
9
+ os.execute(cmd)
10
+ end,
11
+ }
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ # CPU usage (normalized to 100%)
3
+ CORES=$(sysctl -n hw.logicalcpu)
4
+ CPU=$(ps -A -o %cpu | awk -v c="$CORES" '{s+=$1} END {printf "%.0f", s/c}')
5
+ # Memory usage
6
+ MEM=$(ps -A -o %mem | awk '{s+=$1} END {printf "%.0f", s}')
7
+ echo "CPU ${CPU}% MEM ${MEM}%"
@@ -0,0 +1,15 @@
1
+ #!/bin/bash
2
+ # Start tmux with just a terminal (no file explorer)
3
+ TMUX=$(which tmux)
4
+
5
+ # Use first argument as starting dir, or current dir, or home
6
+ DIR="${1:-${PWD:-$HOME}}"
7
+
8
+ # Kill old session if it exists to get a clean start
9
+ $TMUX kill-session -t main 2>/dev/null
10
+
11
+ # Create new session in the target directory (just the shell)
12
+ $TMUX new-session -d -s main -c "$DIR"
13
+
14
+ # Attach
15
+ $TMUX attach-session -t main
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ # Open URL bar — kills current browser and prompts for new URL
3
+ T=$(which tmux)
4
+ $T kill-window -t browser 2>/dev/null
5
+ $T command-prompt -p "URL:" "run-shell \"~/.local/bin/open-browser.sh '%%'\""
6
+ exit 0
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ # Close the browser window
3
+ T=$(which tmux)
4
+ $T kill-window -t browser 2>/dev/null
5
+ exit 0
@@ -0,0 +1,18 @@
1
+ #!/bin/bash
2
+ # Open carbonyl with auto https://
3
+ T=$(which tmux)
4
+ CARBONYL_DIR="$HOME/.local/lib/node_modules/carbonyl/node_modules/@fathyb/carbonyl-macos-arm64/build"
5
+
6
+ URL="$1"
7
+
8
+ # Add protocol if not specified
9
+ if [[ "$URL" != http://* && "$URL" != https://* ]]; then
10
+ if [[ "$URL" == localhost* || "$URL" == 127.0.0.1* ]]; then
11
+ URL="http://$URL"
12
+ else
13
+ URL="https://$URL"
14
+ fi
15
+ fi
16
+
17
+ $T new-window -n browser -e TERM=xterm-256color "cd '$CARBONYL_DIR' && ./carbonyl --zoom=50 '$URL'"
18
+ exit 0
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+ # Toggle carbonyl browser as a full tmux window
3
+ T=$(which tmux)
4
+ CARBONYL_DIR="$HOME/.local/lib/node_modules/carbonyl/node_modules/@fathyb/carbonyl-macos-arm64/build"
5
+
6
+ # Check if browser window exists
7
+ browser_win=$($T list-windows -F '#{window_index} #{window_name}' 2>/dev/null | grep browser | head -1 | awk '{print $1}')
8
+
9
+ if [ -n "$browser_win" ]; then
10
+ # Browser exists — toggle between browser and terminal
11
+ current=$($T display-message -p '#{window_name}')
12
+ if [ "$current" = "browser" ]; then
13
+ $T select-window -t 1
14
+ else
15
+ $T select-window -t browser
16
+ fi
17
+ else
18
+ # Prompt for URL, launch browser
19
+ $T command-prompt -p "URL:" "run-shell \"~/.local/bin/open-browser.sh '%%'\""
20
+ fi
21
+ exit 0
@@ -0,0 +1,12 @@
1
+ #!/bin/bash
2
+ # Toggle yazi file explorer pane in tmux
3
+ T=$(which tmux)
4
+ pane_count=$($T display-message -p '#{window_panes}')
5
+ if [ "$pane_count" -gt 1 ]; then
6
+ $T kill-pane -t {right}
7
+ else
8
+ DIR=$($T display-message -p '#{pane_current_path}')
9
+ $T split-window -h -l 30% -c "$DIR" "env TERM=xterm-256color $(which yazi) $DIR"
10
+ $T select-pane -t 0
11
+ fi
12
+ exit 0