Skip to content

Commit

Permalink
Update tmux-sessioner extension (raycast#6502)
Browse files Browse the repository at this point in the history
* Add tmux-sessioner extension

- add feature default terminal app
- fix PATH is hard code
- Better error message
- add command icon
- simple version

* better description form

* implement new command to change default terminal app

* Update tmux-sessioner extension

- filter only terminal apps
- remove unused code

* update the flow to avoid laggy rendering

* Update tmux-sessioner extension

- Merge pull request #1 from louishuyng/chore/show-hud
- chore: should hud after selecting session

* better description and title command

* Update tmux-sessioner extension

- rename tmux session
- delete tmux session
- create new session command
- create new session command

* Update tmux-sessioner extension

- Update README.md
- Merge pull request #3 from louishuyng/manipulate-basic-tmux-actions

* Update tmux-sessioner extension

- Better icon display
- Update How to Use section in README.md

* Update index.tsx

* Update create_new_session.tsx

* metadata

* Update tmux-sessioner extension

- fix merge conflict
- Pull contributions
- create tmux manage windows command

* Update tmux-sessioner extension

- fix build conflict
- Pull contributions

* delete tmux window action

---------

Co-authored-by: Per Nielsen Tikær <per@raycast.com>
  • Loading branch information
louishuyng and pernielsentikaer authored May 15, 2023
1 parent a6e6000 commit 21c4773
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 29 deletions.
7 changes: 7 additions & 0 deletions extensions/tmux-sessioner/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Tmux Sessioner Changelog

## [v0.0.1] - 2021-05-12
### Added
- Allow Switching between windows 🔄
### Fixed
- Fix code structure to be more readable 📝
- Refactor utils folder

## [Initial Version] - 2023-04-26
5 changes: 4 additions & 1 deletion extensions/tmux-sessioner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This is a extension for raycast to manage tmux sessions.

## Features
- Switch between sessions 🔄
- Switch between windows 🔄
- Attach to sessions/windows automatically with setup terminal 🖥
- Attach to sessions automatically with setup terminal 🖥
- Create new sessions 🆕
- Delete sessions 🗑
Expand All @@ -24,4 +26,5 @@ This is a extension for raycast to manage tmux sessions.
## TODO
- [ ] Label sessions 🏷
- [ ] Prioritize sessions 📈
- [ ] Allow Creating Session with predefined windows 🖼
- [ ] Allow Creating Session with predefined windows 🖼
- [ ] Create/Delete/Rename Windows
6 changes: 6 additions & 0 deletions extensions/tmux-sessioner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
"title": "Create New Session",
"description": "Create New Tmux Session",
"mode": "view"
},
{
"name": "manage_tmux_windows",
"title": "Manage Tmux Windows",
"description": "Manage Tmux Windows that user can switch or delete any tmux window from here",
"mode": "view"
}
],
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion extensions/tmux-sessioner/src/RenameTmuxSession.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Action, ActionPanel, Form, Toast, showToast, useNavigation } from "@raycast/api";

import { useState } from "react";
import { getAllSession, renameSession } from "./sessionUtils";
import { getAllSession, renameSession } from "./utils/sessionUtils";

export const RenameTmuxSession = ({ session, callback }: { session: string; callback?: () => void }) => {
const [loading, setLoading] = useState(false);
Expand Down
2 changes: 1 addition & 1 deletion extensions/tmux-sessioner/src/SelectTermnialApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Action, ActionPanel, Application, Form, Toast, getApplications, showToa
import { LocalStorage } from "@raycast/api";

import { useEffect, useState } from "react";
import { applicationIconFromPath } from "./pathUtils";
import { applicationIconFromPath } from "./utils/pathUtils";

const ALLOWED_APPS = ["kitty", "Alacritty", "iTerm2", "Terminal", "Warp"];

Expand Down
2 changes: 1 addition & 1 deletion extensions/tmux-sessioner/src/create_new_session.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";
import { Action, ActionPanel, Form, popToRoot, showToast, Toast } from "@raycast/api";
import { creatNewSession, getAllSession } from "./sessionUtils";
import { creatNewSession, getAllSession } from "./utils/sessionUtils";

export default function CreateNewTmuxSession() {
const [loading, setLoading] = useState<boolean>(false);
Expand Down
30 changes: 5 additions & 25 deletions extensions/tmux-sessioner/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,17 @@
import { useState, useEffect } from "react";
import { List, Icon, Action, ActionPanel, Toast, showToast, Detail, useNavigation } from "@raycast/api";
import { LocalStorage } from "@raycast/api";

import { List, Icon, Action, ActionPanel, Detail, useNavigation } from "@raycast/api";
import { SelectTerminalApp } from "./SelectTermnialApp";
import { deleteSession, getAllSession, switchToSession } from "./sessionUtils";
import { RenameTmuxSession } from "./RenameTmuxSession";
import { deleteSession, getAllSession, switchToSession } from "./utils/sessionUtils";
import { checkTerminalSetup } from "./utils/terminalUtils";

export default function Command() {
const [sessions, setSessions] = useState<Array<string>>([]);
const [isLoading, setIsLoading] = useState(true);
const [isTerminalSetup, setIsTerminalSetup] = useState(false);

const { push } = useNavigation();
async function checkTerminalSetup(): Promise<boolean> {
const localTerminalAppName = await LocalStorage.getItem<string>("terminalAppName");

const toast = await showToast({
style: Toast.Style.Animated,
title: "Checking terminal App setup",
});

if (!localTerminalAppName) {
toast.style = Toast.Style.Failure;
toast.title = "No default terminal setup for tmux sessioner";
setIsTerminalSetup(false);

return false;
} else {
toast.hide();
setIsTerminalSetup(true);

return true;
}
}

const setupListSesssions = () => {
getAllSession((error, stdout) => {
Expand All @@ -55,7 +35,7 @@ export default function Command() {
(async () => {
setIsLoading(true);

const isSetup = await checkTerminalSetup();
const isSetup = await checkTerminalSetup(setIsTerminalSetup);

if (!isSetup) {
setIsLoading(false);
Expand Down
104 changes: 104 additions & 0 deletions extensions/tmux-sessioner/src/manage_tmux_windows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useState, useEffect } from "react";
import { List, Icon, Action, ActionPanel, Detail } from "@raycast/api";
import { SelectTerminalApp } from "./SelectTermnialApp";
import { checkTerminalSetup } from "./utils/terminalUtils";
import { getAllWindow, switchToWindow, TmuxWindow, deleteWindow } from "./utils/windowUtils";

export default function ManageTmuxWindows() {
const [windows, setWindows] = useState<Array<TmuxWindow & { keyIndex: number }>>([]);
const [isLoading, setIsLoading] = useState(true);
const [isTerminalSetup, setIsTerminalSetup] = useState(false);

const setupListWindows = () => {
getAllWindow((error, stdout) => {
if (error) {
console.error(`exec error: ${error}`);
setIsLoading(false);
return;
}

const lines = stdout.trim().split("\n");

if (lines?.length > 0) {
let keyIndex = 0;
const windows = lines.map((line) => {
const [sessionName, windowName, windowIndex] = line.split(":");
keyIndex += 1; // NOTE: using key index for easily delete and remove window outside the original list
return {
keyIndex,
sessionName,
windowIndex: parseInt(windowIndex),
windowName,
};
});

setWindows(windows);
}

setIsLoading(false);
});
};

useEffect(() => {
(async () => {
setIsLoading(true);

const isSetup = await checkTerminalSetup(setIsTerminalSetup);

if (!isSetup) {
setIsLoading(false);
return;
}
})();
}, []);

useEffect(() => {
if (!isTerminalSetup) {
return;
}

// List down all tmux session
setIsLoading(true);
setupListWindows();
}, [isTerminalSetup]);

return (
<>
<List isLoading={isLoading}>
{windows.map((window, index) => (
<List.Item
key={index}
icon={Icon.Window}
title={window.windowName}
subtitle={window.sessionName}
actions={
<ActionPanel>
<Action title="Switch To Selected Window" onAction={() => switchToWindow(window, setIsLoading)} />
<Action
title="Delete This Window"
onAction={() =>
deleteWindow(window, setIsLoading, () =>
setWindows(windows.filter((w) => w.keyIndex !== window.keyIndex))
)
}
shortcut={{ modifiers: ["cmd"], key: "d" }}
/>
</ActionPanel>
}
/>
))}
</List>

{!isTerminalSetup && !isLoading && (
<Detail
markdown="**Setup Default Terminal App Before Usage** `Go to Actions or using Cmd + k`"
actions={
<ActionPanel>
<Action.Push title="Setup Here" target={<SelectTerminalApp setIsTerminalSetup={setIsTerminalSetup} />} />
</ActionPanel>
}
/>
)}
</>
);
}
43 changes: 43 additions & 0 deletions extensions/tmux-sessioner/src/utils/pathUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Inspired in here https://github.com/raycast/extensions/blob/bbde227e17134f245eff10e59c8a7c2556da976c/extensions/quit-applications/src/index.tsx#L6

import { execSync } from "child_process";

export function applicationIconFromPath(path: string): string {
/* Example:
* '/Applications/Visual Studio Code.app' -> '/Applications/Visual Studio Code.app/Contents/Resources/{file name}.icns'
*/

// read path/Contents/Info.plist and look for <key>CFBundleIconFile</key> or <key>CFBundleIconName</key>
// the actual icon file is located at path/Contents/Resources/{file name}.icns

const infoPlist = `${path}/Contents/Info.plist`;

const possibleIconKeyNames = ["CFBundleIconFile", "CFBundleIconName"];

let iconFileName = null;

for (const keyName of possibleIconKeyNames) {
try {
iconFileName = execSync(["plutil", "-extract", keyName, "raw", '"' + infoPlist + '"'].join(" "))
.toString()
.trim();
break;
} catch (error) {
continue;
}
}

if (!iconFileName) {
// no icon found. fallback to empty string (no icon)
return "";
}

// if icon doesn't end with .icns, add it
if (!iconFileName.endsWith(".icns")) {
iconFileName = `${iconFileName}.icns`;
}

const iconPath = `${path}/Contents/Resources/${iconFileName}`;
console.log(iconPath);
return iconPath;
}
79 changes: 79 additions & 0 deletions extensions/tmux-sessioner/src/utils/sessionUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ChildProcess, exec, ExecException, execSync } from "child_process";
import { env } from "../config";
import { LocalStorage, showHUD, showToast, Toast } from "@raycast/api";
import { openTerminal } from "./terminalUtils";

export function getAllSession(
callback: (error: ExecException | null, stdout: string, stderr: string) => void
): ChildProcess {
return exec(`tmux list-sessions | awk '{print $1}' | sed 's/://'`, { env }, callback);
}

export function creatNewSession(
sessionName: string,
callback: (error: ExecException | null, stdout: string, stderr: string) => void
): ChildProcess {
return exec(`tmux new-session -d -s ${sessionName}`, { env }, callback);
}

export function renameSession(
oldSessionName: string,
newSessionName: string,
callback: (error: ExecException | null, stdout: string, stderr: string) => void
): ChildProcess {
return exec(`tmux rename-session -t ${oldSessionName} ${newSessionName}`, { env }, callback);
}

export async function switchToSession(session: string, setLoading: (value: boolean) => void) {
const toast = await showToast({ style: Toast.Style.Animated, title: "" });
setLoading(true);

exec(`tmux switch -t ${session}`, { env }, async (error, stdout, stderr) => {
if (error || stderr) {
console.error(`exec error: ${error || stderr}`);

toast.style = Toast.Style.Failure;
toast.title = "No tmux client found 😢";
toast.message = error ? error.message : stderr;
setLoading(false);

return;
}

try {
await openTerminal();

toast.style = Toast.Style.Success;
toast.title = `Switched to session ${session}`;
await showHUD(`Switched to session ${session}`);
setLoading(false);
} catch (e) {
toast.style = Toast.Style.Failure;
toast.title = "Terminal not supported 😢";
setLoading(false);
}
return;
});
}

export async function deleteSession(session: string, setLoading: (value: boolean) => void, callback: () => void) {
setLoading(true);
const toast = await showToast({ style: Toast.Style.Animated, title: "" });

exec(`tmux kill-session -t ${session}`, { env }, (error, stdout, stderr) => {
if (error || stderr) {
console.error(`exec error: ${error || stderr}`);

toast.style = Toast.Style.Failure;
toast.title = "Something went wrong 😢";
toast.message = error ? error.message : stderr;
setLoading(false);
return;
}

toast.style = Toast.Style.Success;
toast.title = `Deleted session ${session}`;
callback();
setLoading(false);
});
}
29 changes: 29 additions & 0 deletions extensions/tmux-sessioner/src/utils/terminalUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { LocalStorage, showToast, Toast } from "@raycast/api";
import { execSync } from "child_process";

export async function checkTerminalSetup(callback: (isTerminalSetup: boolean) => void): Promise<boolean> {
const localTerminalAppName = await LocalStorage.getItem<string>("terminalAppName");

const toast = await showToast({
style: Toast.Style.Animated,
title: "Checking terminal App setup",
});

if (!localTerminalAppName) {
toast.style = Toast.Style.Failure;
toast.title = "No default terminal setup for tmux sessioner";
callback(false);

return false;
} else {
toast.hide();
callback(true);

return true;
}
}

export async function openTerminal() {
const localTerminalAppName = await LocalStorage.getItem<string>("terminalAppName");
execSync(`open -a ${localTerminalAppName}`);
}
Loading

0 comments on commit 21c4773

Please sign in to comment.