Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 18 additions & 19 deletions app/contexts/TerminalSessionsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,30 @@ import { SSHHost } from "@/types";
import { router } from "expo-router";
import { useAppContext } from "@/app/AppContext";

export type SessionType =
| "terminal"
| "stats"
| "filemanager"
| "tunnel"
| "remoteDesktop";

export interface TerminalSession {
id: string;
host: SSHHost;
title: string;
isActive: boolean;
createdAt: Date;
type: "terminal" | "stats" | "filemanager" | "tunnel";
type: SessionType;
}

interface TerminalSessionsContextType {
sessions: TerminalSession[];
activeSessionId: string | null;
addSession: (
host: SSHHost,
type?: "terminal" | "stats" | "filemanager" | "tunnel",
) => string;
addSession: (host: SSHHost, type?: SessionType) => string;
removeSession: (sessionId: string) => void;
setActiveSession: (sessionId: string) => void;
clearAllSessions: () => void;
navigateToSessions: (
host?: SSHHost,
type?: "terminal" | "stats" | "filemanager" | "tunnel",
) => void;
navigateToSessions: (host?: SSHHost, type?: SessionType) => void;
isCustomKeyboardVisible: boolean;
toggleCustomKeyboard: () => void;
lastKeyboardHeight: number;
Expand Down Expand Up @@ -72,10 +73,7 @@ export const TerminalSessionsProvider: React.FC<
const [, forceUpdate] = useState({});

const addSession = useCallback(
(
host: SSHHost,
type: "terminal" | "stats" | "filemanager" | "tunnel" = "terminal",
): string => {
(host: SSHHost, type: SessionType = "terminal"): string => {
setSessions((prev) => {
const existingSessions = prev.filter(
(session) => session.host.id === host.id && session.type === type,
Expand All @@ -88,7 +86,9 @@ export const TerminalSessionsProvider: React.FC<
? "Files"
: type === "tunnel"
? "Tunnels"
: "";
: type === "remoteDesktop"
? "Remote"
: "";
let title = typeLabel ? `${host.name} - ${typeLabel}` : host.name;
if (existingSessions.length > 0) {
title = typeLabel
Expand Down Expand Up @@ -156,7 +156,9 @@ export const TerminalSessionsProvider: React.FC<
? "Files"
: session.type === "tunnel"
? "Tunnels"
: "";
: session.type === "remoteDesktop"
? "Remote"
: "";
const baseName = typeLabel
? `${session.host.name} - ${typeLabel}`
: session.host.name;
Expand Down Expand Up @@ -208,10 +210,7 @@ export const TerminalSessionsProvider: React.FC<
);

const navigateToSessions = useCallback(
(
host?: SSHHost,
type: "terminal" | "stats" | "filemanager" | "tunnel" = "terminal",
) => {
(host?: SSHHost, type: SessionType = "terminal") => {
if (host) {
addSession(host, type);
}
Expand Down
32 changes: 28 additions & 4 deletions app/main-axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import type {
SSHHostData,
TunnelConfig,
TunnelStatus,
Credential,
CredentialData,
HostInfo,
ApiResponse,
FileManagerFile,
FileManagerShortcut,
ServerStatus,
Expand Down Expand Up @@ -287,6 +283,23 @@ export function getCurrentServerUrl(): string | null {
return configuredServerUrl;
}

export function getGuacamoleWebSocketUrl(
token: string,
width?: number,
height?: number,
): string {
const base = getRootBase(8081).replace(/\/$/, "");
const websocketBase = base.replace(/^http/i, (scheme) =>
scheme.toLowerCase() === "https" ? "wss" : "ws",
);
const params = new URLSearchParams({ token });

if (width) params.set("width", String(width));
if (height) params.set("height", String(height));

return `${websocketBase}/guacamole/websocket/?${params.toString()}`;
}

export async function isAuthenticated(): Promise<boolean> {
try {
const token = await getCookie("jwt");
Expand Down Expand Up @@ -822,6 +835,17 @@ export async function exportSSHHostWithCredentials(
}
}

export async function getGuacamoleTokenFromHost(
hostId: number,
): Promise<{ token: string }> {
try {
const response = await authApi.post(`/guacamole/connect-host/${hostId}`);
return response.data;
} catch (error) {
handleApiError(error, "connect Guacamole host");
}
}

// ============================================================================
// SSH AUTOSTART MANAGEMENT
// ============================================================================
Expand Down
2 changes: 1 addition & 1 deletion app/tabs/hosts/Hosts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default function Hosts() {
});
}

hosts.filter((host: SSHHost) => !host.connectionType || host.connectionType === "ssh").forEach((host: SSHHost) => {
hosts.forEach((host: SSHHost) => {
const folderName = host.folder || "No Folder";
if (!folderMap.has(folderName)) {
folderMap.set(folderName, {
Expand Down
42 changes: 37 additions & 5 deletions app/tabs/hosts/navigation/Host.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@ import {
} from "react-native";
import {
Terminal,
Server,
FolderOpen,
Key,
Lock,
MoreVertical,
X,
Activity,
Monitor,
} from "lucide-react-native";
import { SSHHost } from "@/types";
import { useTerminalSessions } from "@/app/contexts/TerminalSessionsContext";
Expand All @@ -37,6 +35,14 @@ function Host({ host, status, isLast = false }: HostProps) {
const [tagsContainerWidth, setTagsContainerWidth] = useState<number>(0);
const statusLabel =
status === "online" ? "UP" : status === "offline" ? "DOWN" : "UNK";
const connectionType = (host.connectionType || "ssh").toLowerCase();
const isRemoteDesktopHost = ["rdp", "vnc", "telnet"].includes(connectionType);
const remoteDesktopLabel =
connectionType === "vnc"
? "Open VNC Session"
: connectionType === "telnet"
? "Open Telnet Session"
: "Open RDP Session";

const parsedStatsConfig: StatsConfig = (() => {
try {
Expand Down Expand Up @@ -114,6 +120,11 @@ function Host({ host, status, isLast = false }: HostProps) {
setShowContextMenu(false);
};

const handleRemoteDesktopPress = () => {
navigateToSessions(host, "remoteDesktop");
setShowContextMenu(false);
};

const handleStatsPress = () => {
navigateToSessions(host, "stats");
setShowContextMenu(false);
Expand All @@ -136,7 +147,7 @@ function Host({ host, status, isLast = false }: HostProps) {
<>
<TouchableOpacity
className="p-3 bg-dark-bg-darker rounded-md border-2 border-dark-border"
onPress={handleTerminalPress}
onPress={isRemoteDesktopHost ? handleRemoteDesktopPress : handleTerminalPress}
activeOpacity={0.7}
>
<View className="flex flex-row items-center">
Expand Down Expand Up @@ -361,7 +372,28 @@ function Host({ host, status, isLast = false }: HostProps) {

<ScrollView showsVerticalScrollIndicator={false}>
<View className="gap-2">
{host.enableTerminal && (
{isRemoteDesktopHost && (
<TouchableOpacity
onPress={handleRemoteDesktopPress}
className="flex-row items-center gap-3 p-3 rounded-md bg-dark-bg-darker border border-dark-border"
activeOpacity={0.7}
>
<Monitor size={20} color="#FFFFFF" />
<View className="flex-1">
<Text className="text-white font-medium">
{remoteDesktopLabel}
</Text>
<Text
className="text-gray-400 text-xs"
numberOfLines={1}
>
{host.ip}:{host.port}
</Text>
</View>
</TouchableOpacity>
)}

{host.enableTerminal && !isRemoteDesktopHost && (
<TouchableOpacity
onPress={handleTerminalPress}
className="flex-row items-center gap-3 p-3 rounded-md bg-dark-bg-darker border border-dark-border"
Expand Down
39 changes: 21 additions & 18 deletions app/tabs/sessions/Sessions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,9 @@ import React, { useState, useEffect, useRef, useCallback } from "react";
import {
View,
Text,
ScrollView,
TouchableOpacity,
StyleSheet,
Keyboard,
KeyboardAvoidingView,
Platform,
TextInput,
TouchableWithoutFeedback,
Pressable,
Dimensions,
BackHandler,
AppState,
Expand All @@ -19,7 +13,10 @@ import {
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useFocusEffect } from "@react-navigation/native";
import { useRouter } from "expo-router";
import { useTerminalSessions } from "@/app/contexts/TerminalSessionsContext";
import {
SessionType,
useTerminalSessions,
} from "@/app/contexts/TerminalSessionsContext";
import { useKeyboard } from "@/app/contexts/KeyboardContext";
import {
Terminal,
Expand All @@ -37,17 +34,13 @@ import {
TunnelManager,
TunnelManagerHandle,
} from "@/app/tabs/sessions/tunnel/TunnelManager";
import { RemoteDesktop } from "@/app/tabs/sessions/remote-desktop/RemoteDesktop";
import TabBar from "@/app/tabs/sessions/navigation/TabBar";
import BottomToolbar from "@/app/tabs/sessions/terminal/keyboard/BottomToolbar";
import KeyboardBar from "@/app/tabs/sessions/terminal/keyboard/KeyboardBar";
import { ArrowLeft } from "lucide-react-native";
import { useOrientation } from "@/app/utils/orientation";
import { getMaxKeyboardHeight, getTabBarHeight } from "@/app/utils/responsive";
import {
BACKGROUNDS,
BORDER_COLORS,
BORDERS,
} from "@/app/constants/designTokens";
import { BACKGROUNDS, BORDER_COLORS } from "@/app/constants/designTokens";
import { addKeyCommandListener } from "@/modules/hardware-keyboard";

export default function Sessions() {
Expand Down Expand Up @@ -125,6 +118,10 @@ export default function Sessions() {
const KEYBOARD_BAR_HEIGHT = isLandscape ? 48 : 52;
const KEYBOARD_BAR_HEIGHT_EXTENDED = isLandscape ? 64 : 68;

const activeSession = sessions.find(
(session) => session.id === activeSessionId,
);

const getTabBarBottomPosition = () => {
if (activeSession?.type !== "terminal") {
return insets.bottom;
Expand All @@ -146,7 +143,7 @@ export default function Sessions() {
};

const getBottomMargin = (
sessionType: "terminal" | "stats" | "filemanager" = "terminal",
sessionType: SessionType = "terminal",
) => {
if (sessionType !== "terminal") {
return SESSION_TAB_BAR_HEIGHT + insets.bottom;
Expand Down Expand Up @@ -523,10 +520,6 @@ export default function Sessions() {
[],
);

const activeSession = sessions.find(
(session) => session.id === activeSessionId,
);

const activeTerminalBgColor =
activeSession?.type === "terminal" && activeSessionId
? terminalBackgroundColors[activeSessionId] || BACKGROUNDS.DARKEST
Expand Down Expand Up @@ -625,6 +618,16 @@ export default function Sessions() {
onClose={() => handleTabClose(session.id)}
/>
);
} else if (session.type === "remoteDesktop") {
return (
<RemoteDesktop
key={session.id}
host={session.host}
isVisible={session.id === activeSessionId}
title={session.title}
onClose={() => handleTabClose(session.id)}
/>
);
}
return null;
})}
Expand Down
7 changes: 5 additions & 2 deletions app/tabs/sessions/navigation/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
ChevronDown,
ChevronUp,
} from "lucide-react-native";
import { TerminalSession } from "@/app/contexts/TerminalSessionsContext";
import {
SessionType,
TerminalSession,
} from "@/app/contexts/TerminalSessionsContext";
import { useRouter } from "expo-router";
import { useKeyboard } from "@/app/contexts/KeyboardContext";
import { useOrientation } from "@/app/utils/orientation";
Expand All @@ -40,7 +43,7 @@ interface TabBarProps {
onHideKeyboard?: () => void;
onShowKeyboard?: () => void;
keyboardIntentionallyHiddenRef: React.MutableRefObject<boolean>;
activeSessionType?: "terminal" | "stats" | "filemanager";
activeSessionType?: SessionType;
}

export default function TabBar({
Expand Down
Loading