@@ -1,9 +1,8 @@
package interaction
import (
"bytes "
"context "
"fmt"
"io"
"log"
"strings"
"time"
@@ -11,6 +10,13 @@ import (
"tunnel_pls/types"
"tunnel_pls/utils"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"golang.org/x/crypto/ssh"
)
@@ -19,20 +25,10 @@ type Lifecycle interface {
}
type Controller interface {
SendMessage ( message string )
HandleUserInput ( )
HandleCommand ( command string )
HandleSlugEditMode ( char byte )
HandleSlugSave ( )
HandleSlugCancel ( )
HandleSlugUpdateError ( )
ShowWelcomeMessage ( )
DisplaySlugEditor ( )
SetChannel ( channel ssh . Channel )
SetLifecycle ( lifecycle Lifecycle )
SetSlugModificator ( func ( oldSlug , newSlug string ) bool )
WaitForKeyPress ( )
ShowForwardingMessage ( )
Start ( )
}
type Forwarder interface {
@@ -42,32 +38,58 @@ type Forwarder interface {
}
type Interaction struct {
inputLength int
commandBuffer * bytes . Buffer
interactiveMode bool
interactionType types . InteractionType
editSlug string
channel ssh . Channel
slugManager slug . Manager
forwarder Forwarder
lifecycle Lifecycle
pendingExit bool
updateClientSlug func ( oldSlug , newSlug string ) bool
program * tea . Program
ctx context . Context
cancel context . CancelFunc
}
type commandItem struct {
name string
desc string
}
type model struct {
tunnelURL string
domain string
protocol string
tunnelType types . TunnelType
port uint16
keymap keymap
help help . Model
quitting bool
showingCommands bool
editingSlug bool
showingComingSoon bool
commandList list . Model
slugInput textinput . Model
slugError string
interaction * Interaction
}
type keymap struct {
quit key . Binding
command key . Binding
random key . Binding
}
type tickMsg time . Time
func NewInteraction ( slugManager slug . Manager , forwarder Forwarder ) * Interaction {
ctx , cancel := context . WithCancel ( context . Background ( ) )
return & Interaction {
inputLength : 0 ,
commandBuffer : bytes . NewBuffer ( make ( [ ] byte , 0 , 20 ) ) ,
interactiveMode : false ,
interactionType : "" ,
editSlug : "" ,
channel : nil ,
slugManager : slugManager ,
forwarder : forwarder ,
lifecycle : nil ,
pendingExit : false ,
updateClientSlug : nil ,
program : nil ,
ctx : ctx ,
cancel : cancel ,
}
}
@@ -79,414 +101,439 @@ func (i *Interaction) SetChannel(channel ssh.Channel) {
i . channel = channel
}
func ( i * Interaction ) SendMessage ( message string ) {
if i . channel == nil {
log . Printf ( "channel is nil" )
}
_ , err := i . channel . Write ( [ ] byte ( message ) )
if err != nil && err != io . EOF {
log . Printf ( "error writing to channel: %s" , err )
}
return
}
func ( i * Interaction ) HandleUserInput ( ) {
buf := make ( [ ] byte , 1 )
i . interactiveMode = false
for {
n , err := i . channel . Read ( buf )
if err != nil {
i . handleReadError ( err )
break
}
if n > 0 {
i . processCharacter ( buf [ 0 ] )
}
}
}
func ( i * Interaction ) handleReadError ( err error ) {
if err != io . EOF {
log . Printf ( "Error reading from client: %s" , err )
}
}
func ( i * Interaction ) processCharacter ( char byte ) {
if i . interactiveMode {
i . handleInteractiveMode ( char )
return
}
if i . handleExitSequence ( char ) {
return
}
i . SendMessage ( string ( char ) )
i . handleNonInteractiveInput ( char )
}
func ( i * Interaction ) handleInteractiveMode ( char byte ) {
switch i . interactionType {
case types . Slug :
i . HandleSlugEditMode ( char )
}
}
func ( i * Interaction ) handleExitSequence ( char byte ) bool {
if char == ctrlC {
if i . pendingExit {
i . SendMessage ( "Closing connection...\r\n" )
if err := i . lifecycle . Close ( ) ; err != nil {
log . Printf ( "failed to close session: %v" , err )
}
return true
}
i . SendMessage ( "Please press Ctrl+C again to disconnect.\r\n" )
i . pendingExit = true
return true
}
if i . pendingExit && char != ctrlC {
i . pendingExit = false
i . SendMessage ( "Operation canceled.\r\n" )
}
return false
}
func ( i * Interaction ) handleNonInteractiveInput ( char byte ) {
switch {
case char == backspaceChar || char == deleteChar :
i . handleBackspace ( )
case char == forwardSlash :
i . handleCommandStart ( )
case i . commandBuffer . Len ( ) > 0 :
i . handleCommandInput ( char )
case char == enterChar :
i . SendMessage ( clearLine )
default :
i . inputLength ++
}
}
func ( i * Interaction ) handleBackspace ( ) {
if i . inputLength > 0 {
i . SendMessage ( backspaceSeq )
}
if i . commandBuffer . Len ( ) > 0 {
i . commandBuffer . Truncate ( i . commandBuffer . Len ( ) - 1 )
}
}
func ( i * Interaction ) handleCommandStart ( ) {
i . commandBuffer . Reset ( )
i . commandBuffer . WriteByte ( forwardSlash )
}
func ( i * Interaction ) handleCommandInput ( char byte ) {
if char == enterChar {
i . SendMessage ( clearLine )
i . HandleCommand ( i . commandBuffer . String ( ) )
return
}
i . commandBuffer . WriteByte ( char )
i . inputLength ++
}
func ( i * Interaction ) HandleSlugEditMode ( char byte ) {
switch {
case char == enterChar :
i . HandleSlugSave ( )
case char == escapeChar || char == ctrlC :
i . HandleSlugCancel ( )
case char == backspaceChar || char == deleteChar :
i . handleSlugBackspace ( )
case char >= minPrintableChar && char <= maxPrintableChar :
i . appendToSlug ( char )
}
}
func ( i * Interaction ) handleSlugBackspace ( ) {
if len ( i . editSlug ) > 0 {
i . editSlug = i . editSlug [ : len ( i . editSlug ) - 1 ]
i . refreshSlugDisplay ( )
}
}
func ( i * Interaction ) appendToSlug ( char byte ) {
if len ( i . editSlug ) < maxSlugLength {
i . editSlug += string ( char )
i . refreshSlugDisplay ( )
}
}
func ( i * Interaction ) refreshSlugDisplay ( ) {
domain := utils . Getenv ( "DOMAIN" , "localhost" )
i . SendMessage ( clearToLineEnd )
i . SendMessage ( "➤ " + i . editSlug + "." + domain )
}
func ( i * Interaction ) HandleSlugSave ( ) {
i . SendMessage ( clearScreen )
switch {
case isForbiddenSlug ( i . editSlug ) :
i . showForbiddenSlugMessage ( )
case ! isValidSlug ( i . editSlug ) :
i . showInvalidSlugMessage ( )
default :
i . updateSlug ( )
}
i . WaitForKeyPress ( )
i . returnToMainScreen ( )
}
func ( i * Interaction ) updateSlug ( ) {
oldSlug := i . slugManager . Get ( )
newSlug := i . editSlug
if ! i . updateClientSlug ( oldSlug , newSlug ) {
i . HandleSlugUpdateError ( )
return
}
domain := utils . Getenv ( "DOMAIN" , "localhost" )
i . SendMessage ( "\r\n\r\n✅ SUBDOMAIN UPDATED ✅\r\n\r\n" )
i . SendMessage ( "Your new address is: " + newSlug + "." + domain + "\r\n\r\n" )
i . SendMessage ( "Press any key to continue...\r\n" )
}
func ( i * Interaction ) showForbiddenSlugMessage ( ) {
i . SendMessage ( "\r\n\r\n❌ FORBIDDEN SUBDOMAIN ❌\r\n\r\n" )
i . SendMessage ( "This subdomain is not allowed.\r\n" )
i . SendMessage ( "Please try a different subdomain.\r\n\r\n" )
i . SendMessage ( "Press any key to continue...\r\n" )
}
func ( i * Interaction ) showInvalidSlugMessage ( ) {
i . SendMessage ( "\r\n\r\n❌ INVALID SUBDOMAIN ❌\r\n\r\n" )
i . SendMessage ( "Use only lowercase letters, numbers, and hyphens.\r\n" )
i . SendMessage ( fmt . Sprintf ( "Length must be %d-%d characters and cannot start or end with a hyphen.\r\n\r\n" , minSlugLength , maxSlugLength ) )
i . SendMessage ( "Press any key to continue...\r\n" )
}
func ( i * Interaction ) returnToMainScreen ( ) {
i . SendMessage ( clearScreen )
i . ShowWelcomeMessage ( )
i . ShowForwardingMessage ( )
i . interactiveMode = false
i . commandBuffer . Reset ( )
}
func ( i * Interaction ) HandleSlugCancel ( ) {
i . SendMessage ( clearScreen )
i . SendMessage ( "\r\n\r\n⚠️ SUBDOMAIN EDIT CANCELLED ⚠️\r\n\r\n" )
i . SendMessage ( "Press any key to continue...\r\n" )
i . interactiveMode = false
i . interactionType = ""
i . WaitForKeyPress ( )
i . SendMessage ( clearScreen )
i . ShowWelcomeMessage ( )
i . ShowForwardingMessage ( )
}
func ( i * Interaction ) HandleSlugUpdateError ( ) {
i . SendMessage ( "\r\n\r\n❌ SERVER ERROR ❌\r\n\r\n" )
i . SendMessage ( "Failed to update subdomain. You will be disconnected in 5 seconds.\r\n\r\n" )
for countdown := 5 ; countdown > 0 ; countdown -- {
i . SendMessage ( fmt . Sprintf ( "Disconnecting in %d...\r\n" , countdown ) )
time . Sleep ( 1 * time . Second )
}
if err := i . lifecycle . Close ( ) ; err != nil {
log . Printf ( "failed to close session: %v" , err )
}
}
func ( i * Interaction ) HandleCommand ( command string ) {
handlers := map [ string ] func ( ) {
"/bye" : i . handleByeCommand ,
"/help" : i . handleHelpCommand ,
"/clear" : i . handleClearCommand ,
"/slug" : i . handleSlugCommand ,
}
if handler , exists := handlers [ command ] ; exists {
handler ( )
} else {
i . SendMessage ( "Unknown command\r\n" )
}
i . commandBuffer . Reset ( )
}
func ( i * Interaction ) handleByeCommand ( ) {
i . SendMessage ( "Closing connection...\r\n" )
if err := i . lifecycle . Close ( ) ; err != nil {
log . Printf ( "failed to close session: %v" , err )
}
}
func ( i * Interaction ) handleHelpCommand ( ) {
i . SendMessage ( "\r\nAvailable commands: /bye, /help, /clear, /slug\r\n" )
}
func ( i * Interaction ) handleClearCommand ( ) {
i . SendMessage ( clearScreen )
i . ShowWelcomeMessage ( )
i . ShowForwardingMessage ( )
}
func ( i * Interaction ) handleSlugCommand ( ) {
if i . forwarder . GetTunnelType ( ) != types . HTTP {
i . SendMessage ( fmt . Sprintf ( "\r\n%s tunnels cannot have custom subdomains\r\n" , i . forwarder . GetTunnelType ( ) ) )
return
}
i . interactiveMode = true
i . interactionType = types . Slug
i . editSlug = i . slugManager . Get ( )
i . SendMessage ( clearScreen )
i . DisplaySlugEditor ( )
domain := utils . Getenv ( "DOMAIN" , "localhost" )
i . SendMessage ( "➤ " + i . editSlug + "." + domain )
}
func ( i * Interaction ) ShowForwardingMessage ( ) {
domain := utils . Getenv ( "DOMAIN" , "localhost" )
if i . forwarder . GetTunnelType ( ) == types . HTTP {
protocol := "http"
if utils . Getenv ( "TLS_ENABLED" , "false" ) == "true" {
protocol = "https"
}
i . SendMessage ( fmt . Sprintf ( "Forwarding your traffic to %s://%s.%s \r\n" , protocol , i . slugManager . Get ( ) , domain ) )
} else {
i . SendMessage ( fmt . Sprintf ( "Forwarding your traffic to tcp://%s:%d \r\n" , domain , i . forwarder . GetForwardedPort ( ) ) )
}
}
func ( i * Interaction ) ShowWelcomeMessage ( ) {
asciiArt := [ ] string {
` _______ _ _____ _ ` ,
` |__ __| | | | __ \| | ` ,
` | |_ _ _ __ _ __ ___| | | |__) | |___ ` ,
` | | | | | '_ \| '_ \ / _ \ | | ___/| / __| ` ,
` | | |_| | | | | | | | __/ | | | | \__ \ ` ,
` |_|\__,_|_| |_|_| |_|\___|_| |_| |_|___/ ` ,
` ` ,
` "Tunnel Pls" - Project by Bagas ` ,
` https://fossy.my.id ` ,
` ` ,
` Welcome to Tunnel! Available commands: ` ,
` - '/bye' : Exit the tunnel ` ,
` - '/help' : Show this help message ` ,
` - '/clear' : Clear the current line ` ,
` - '/slug' : Set custom subdomain ` ,
}
for _ , line := range asciiArt {
i . SendMessage ( "\r\n" + line )
}
i . SendMessage ( "\r\n\r\n" )
}
func ( i * Interaction ) DisplaySlugEditor ( ) {
domain := utils . Getenv ( "DOMAIN" , "localhost" )
fullDomain := i . slugManager . Get ( ) + "." + domain
contentLine := " ║ Current: " + fullDomain
boxWidth := calculateBoxWidth ( contentLine )
box := buildSlugEditorBox ( boxWidth , fullDomain )
i . SendMessage ( "\r\n\r\n" + box + "\r\n\r\n" )
}
func buildSlugEditorBox ( boxWidth int , fullDomain string ) string {
topBorder := " ╔" + strings . Repeat ( "═" , boxWidth - 4 ) + "╗\r\n"
title := centerText ( "SUBDOMAIN EDITOR" , boxWidth - 4 )
header := " ║" + title + "║\r\n"
midBorder := " ╠" + strings . Repeat ( "═" , boxWidth - 4 ) + "╣\r\n"
emptyLine := " ║" + strings . Repeat ( " " , boxWidth - 4 ) + "║\r\n"
currentLineContent := fmt . Sprintf ( " ║ Current: %s" , fullDomain )
currentLine := currentLineContent + strings . Repeat ( " " , boxWidth - len ( currentLineContent ) + 1 ) + "║\r\n"
saveCancel := " ║ [Enter] Save | [Esc] Cancel" + strings . Repeat ( " " , boxWidth - 35 ) + "║\r\n"
bottomBorder := " ╚" + strings . Repeat ( "═" , boxWidth - 4 ) + "╝\r\n"
return topBorder + header + midBorder + emptyLine + currentLine + emptyLine + emptyLine + midBorder + saveCancel + bottomBorder
}
func ( i * Interaction ) SetSlugModificator ( modificator func ( oldSlug , newSlug string ) bool ) {
i . updateClientSlug = modificator
}
func ( i * Interaction ) WaitForKeyPress ( ) {
keyBuf := make ( [ ] byte , 1 )
for {
_ , err := i . channel . Read ( keyBuf )
if err = = nil {
break
func ( i * Interaction ) Stop ( ) {
if i . cancel != nil {
i . cancel ( )
}
if i . program ! = nil {
i . program . Kill ( )
i . program = nil
}
}
func ( i commandItem ) FilterValue ( ) string { return i . name }
func ( i commandItem ) Title ( ) string { return i . name }
func ( i commandItem ) Description ( ) string { return i . desc }
func tickCmd ( d time . Duration ) tea . Cmd {
return tea . Tick ( d , func ( t time . Time ) tea . Msg {
return tickMsg ( t )
} )
}
func ( m model ) Init ( ) tea . Cmd {
return tea . Batch ( textinput . Blink , tea . WindowSize ( ) )
}
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
var cmd tea . Cmd
switch msg := msg . ( type ) {
case tickMsg :
m . showingComingSoon = false
return m , tea . Batch ( tea . ClearScreen , textinput . Blink )
case tea . WindowSizeMsg :
m . commandList . SetWidth ( msg . Width )
m . commandList . SetHeight ( msg . Height - 4 )
return m , nil
case tea . QuitMsg :
m . quitting = true
return m , tea . Batch ( tea . ClearScreen , textinput . Blink , tea . Quit )
case tea . KeyMsg :
if m . showingComingSoon {
m . showingComingSoon = false
return m , tea . Batch ( tea . ClearScreen , textinput . Blink )
}
if err != nil {
log . Printf ( "Error reading keypress: %v" , err )
break
if m . editingSlug {
switch msg . String ( ) {
case "esc" :
m . editingSlug = false
m . slugError = ""
return m , tea . Batch ( tea . ClearScreen , textinput . Blink )
case "enter" :
inputValue := m . slugInput . Value ( )
m . interaction . updateClientSlug ( m . interaction . slugManager . Get ( ) , inputValue )
m . tunnelURL = buildURL ( m . protocol , inputValue , m . domain )
m . editingSlug = false
m . slugError = ""
return m , tea . Batch ( tea . ClearScreen , textinput . Blink )
case "ctrl+c" :
m . editingSlug = false
m . slugError = ""
return m , tea . Batch ( tea . ClearScreen , textinput . Blink )
default :
if key . Matches ( msg , m . keymap . random ) {
newSubdomain := generateRandomSubdomain ( )
m . slugInput . SetValue ( newSubdomain )
m . slugError = ""
m . slugInput , cmd = m . slugInput . Update ( msg )
return m , cmd
}
m . slugError = ""
m . slugInput , cmd = m . slugInput . Update ( msg )
return m , cmd
}
}
}
}
func calculateBoxWidth ( contentLine string ) int {
boxWidth := len ( contentLine ) + paddingRight + 1
if boxWidth < minBoxWidth {
boxWidth = minBoxWidth
}
return boxWidth
}
if m . showingCommands {
switch {
case key . Matches ( msg , m. keymap . quit ) :
m . showingCommands = false
return m , tea . Batch ( tea . ClearScreen , textinput . Blink )
case msg . String ( ) == "enter" :
selectedItem := m . commandList . SelectedItem ( )
if selectedItem != nil {
item := selectedItem . ( commandItem )
if item . name == "slug" {
m . showingCommands = false
m . editingSlug = true
m . slugInput . SetValue ( m . interaction . slugManager . Get ( ) )
m . slugInput . Focus ( )
return m , tea . Batch ( tea . ClearScreen , textinput . Blink )
} else if item . name == "tunnel-type" {
m . showingCommands = false
m . showingComingSoon = true
return m , tea . Batch ( tickCmd ( 5 * time . Second ) , tea . ClearScreen , textinput . Blink )
}
m . showingCommands = false
return m , nil
}
case msg . String ( ) == "esc" :
m . showingCommands = false
return m , tea . Batch ( tea . ClearScreen , textinput . Blink )
}
m . commandList , cmd = m . commandList . Update ( msg )
return m , cmd
}
func centerText ( text string , width int ) string {
padding := ( width - len ( tex t) ) / 2
if padd ing < 0 {
padding = 0
}
return strings . Repeat ( " " , padding ) + text + strings . Repeat ( " " , width - len ( text ) - padding )
}
func isValidSlug ( slug string ) bool {
if len ( slug ) < minSlugLength || len ( slug ) > maxSlugLength {
return false
}
if slug [ 0 ] == '-' || slug [ len ( slug ) - 1 ] == '-' {
return false
}
for _ , c := range slug {
if ! isValidSlugChar ( byte ( c ) ) {
return false
switch {
case key . Matches ( msg , m . keymap . qui t) :
m . quitt ing = true
return m , tea . Batch ( tea . ClearScreen , textinput . Blink , tea . Quit )
case key . Matches ( msg , m . keymap . command ) :
m . showingCommands = true
return m , tea . Batch ( tea . ClearScreen , textinput . Blink )
}
}
return true
return m , nil
}
func isValidSlugChar ( c byte ) bool {
return ( c >= 'a' && c <= 'z' ) || ( c >= '0' && c <= '9' ) || c == '-'
func ( m model ) helpView ( ) string {
return "\n" + m . help . ShortHelpView ( [ ] key . Binding {
m . keymap . command ,
m . keymap . quit ,
} )
}
func isForbiddenSlug ( slug string ) bool {
for _ , s := range forbiddenSlugs {
if slug == s {
return true
}
func ( m model ) View ( ) string {
i f m . quitting {
return ""
}
return false
if m . showingComingSoon {
titleStyle := lipgloss . NewStyle ( ) .
Bold ( true ) .
Foreground ( lipgloss . Color ( "#7D56F4" ) ) .
PaddingTop ( 1 ) .
PaddingBottom ( 1 )
messageBoxStyle := lipgloss . NewStyle ( ) .
Foreground ( lipgloss . Color ( "#FAFAFA" ) ) .
Background ( lipgloss . Color ( "#1A1A2E" ) ) .
Bold ( true ) .
Border ( lipgloss . RoundedBorder ( ) ) .
BorderForeground ( lipgloss . Color ( "#7D56F4" ) ) .
Padding ( 1 , 3 ) .
MarginTop ( 2 ) .
MarginBottom ( 2 ) .
Align ( lipgloss . Center )
helpStyle := lipgloss . NewStyle ( ) .
Foreground ( lipgloss . Color ( "#666666" ) ) .
Italic ( true ) .
MarginTop ( 1 )
var b strings . Builder
b . WriteString ( "\n\n" )
b . WriteString ( titleStyle . Render ( "⏳ Coming Soon" ) )
b . WriteString ( "\n\n" )
b . WriteString ( messageBoxStyle . Render ( "🚀 This feature is coming very soon!\n Stay tuned for updates." ) )
b . WriteString ( "\n\n" )
b . WriteString ( helpStyle . Render ( "This message will disappear in 5 seconds or press any key..." ) )
return b . String ( )
}
if m . editingSlug {
titleStyle := lipgloss . NewStyle ( ) .
Bold ( true ) .
Foreground ( lipgloss . Color ( "#7D56F4" ) ) .
PaddingTop ( 1 ) .
PaddingBottom ( 1 )
instructionStyle := lipgloss . NewStyle ( ) .
Foreground ( lipgloss . Color ( "#FAFAFA" ) ) .
MarginTop ( 1 )
inputBoxStyle := lipgloss . NewStyle ( ) .
Border ( lipgloss . RoundedBorder ( ) ) .
BorderForeground ( lipgloss . Color ( "#7D56F4" ) ) .
Padding ( 1 , 2 ) .
MarginTop ( 2 ) .
MarginBottom ( 2 )
helpStyle := lipgloss . NewStyle ( ) .
Foreground ( lipgloss . Color ( "#666666" ) ) .
Italic ( true ) .
MarginTop ( 1 )
errorBoxStyle := lipgloss . NewStyle ( ) .
Foreground ( lipgloss . Color ( "#FF0000" ) ) .
Background ( lipgloss . Color ( "#3D0000" ) ) .
Bold ( true ) .
Border ( lipgloss . RoundedBorder ( ) ) .
BorderForeground ( lipgloss . Color ( "#FF0000" ) ) .
Padding ( 0 , 2 ) .
MarginTop ( 1 ) .
MarginBottom ( 1 )
rulesBoxStyle := lipgloss . NewStyle ( ) .
Foreground ( lipgloss . Color ( "#FAFAFA" ) ) .
Border ( lipgloss . RoundedBorder ( ) ) .
BorderForeground ( lipgloss . Color ( "#7D56F4" ) ) .
Padding ( 0 , 2 ) .
MarginTop ( 1 ) .
MarginBottom ( 1 )
var b strings . Builder
b . WriteString ( titleStyle . Render ( "🔧 Edit Subdomain" ) )
b . WriteString ( "\n\n" )
if m . tunnelType != types . HTTP {
warningBoxStyle := lipgloss . NewStyle ( ) .
Foreground ( lipgloss . Color ( "#FFA500" ) ) .
Background ( lipgloss . Color ( "#3D2000" ) ) .
Bold ( true ) .
Border ( lipgloss . RoundedBorder ( ) ) .
BorderForeground ( lipgloss . Color ( "#FFA500" ) ) .
Padding ( 1 , 2 ) .
MarginTop ( 2 ) .
MarginBottom ( 2 )
b . WriteString ( warningBoxStyle . Render ( "⚠️ TCP tunnels cannot have custom subdomains. Only HTTP/HTTPS tunnels support subdomain customization. " ) )
b . WriteString ( "\n\n" )
b . WriteString ( helpStyle . Render ( "Press Enter or Esc to go back" ) )
return b . String ( )
}
rulesContent := "📋 Rules: \n\t• 3-20 chars \n\t• a-z, 0-9, - \n\t• No leading/trailing -"
b . WriteString ( rulesBoxStyle . Render ( rulesContent ) )
b . WriteString ( "\n" )
b . WriteString ( instructionStyle . Render ( "Enter your custom subdomain:" ) )
b . WriteString ( "\n" )
if m . slugError != "" {
errorInputBoxStyle := lipgloss . NewStyle ( ) .
Border ( lipgloss . RoundedBorder ( ) ) .
BorderForeground ( lipgloss . Color ( "#FF0000" ) ) .
Padding ( 1 , 2 ) .
MarginTop ( 2 ) .
MarginBottom ( 1 )
b . WriteString ( errorInputBoxStyle . Render ( m . slugInput . View ( ) ) )
b . WriteString ( "\n" )
b . WriteString ( errorBoxStyle . Render ( "❌ " + m . slugError ) )
b . WriteString ( "\n" )
} else {
b . WriteString ( inputBoxStyle . Render ( m . slugInput . View ( ) ) )
b . WriteString ( "\n" )
}
previewURL := buildURL ( m . protocol , m . slugInput . Value ( ) , m . domain )
previewStyle := lipgloss . NewStyle ( ) .
Foreground ( lipgloss . Color ( "#04B575" ) ) .
Italic ( true ) .
Width ( 80 )
b . WriteString ( previewStyle . Render ( fmt . Sprintf ( "Preview: %s" , previewURL ) ) )
b . WriteString ( "\n" )
b . WriteString ( helpStyle . Render ( "Press Enter to save • CTRL+R for random • Esc to cancel" ) )
return b . String ( )
}
if m . showingCommands {
titleStyle := lipgloss . NewStyle ( ) .
Bold ( true ) .
Foreground ( lipgloss . Color ( "#7D56F4" ) ) .
PaddingTop ( 1 ) .
PaddingBottom ( 1 )
helpStyle := lipgloss . NewStyle ( ) .
Foreground ( lipgloss . Color ( "#666666" ) ) .
Italic ( true ) .
MarginTop ( 1 )
var b strings . Builder
b . WriteString ( "\n" )
b . WriteString ( titleStyle . Render ( "⚡ Commands" ) )
b . WriteString ( "\n\n" )
b . WriteString ( m . commandList . View ( ) )
b . WriteString ( "\n" )
b . WriteString ( helpStyle . Render ( "↑/↓ Navigate • Enter Select • Esc Cancel" ) )
return b . String ( )
}
titleStyle := lipgloss . NewStyle ( ) .
Bold ( true ) .
Foreground ( lipgloss . Color ( "#7D56F4" ) ) .
PaddingTop ( 1 ) .
PaddingBottom ( 1 )
subtitleStyle := lipgloss . NewStyle ( ) .
Foreground ( lipgloss . Color ( "#7D56F4" ) ) .
Italic ( true )
urlStyle := lipgloss . NewStyle ( ) .
Foreground ( lipgloss . Color ( "#04B575" ) ) .
Underline ( true )
sectionTitleStyle := lipgloss . NewStyle ( ) .
Bold ( true ) .
Foreground ( lipgloss . Color ( "#FAFAFA" ) ) .
MarginTop ( 1 ) .
MarginBottom ( 1 )
forwardingStyle := lipgloss . NewStyle ( ) .
Bold ( true ) .
Foreground ( lipgloss . Color ( "#04B575" ) ) .
Border ( lipgloss . RoundedBorder ( ) ) .
BorderForeground ( lipgloss . Color ( "#04B575" ) ) .
Padding ( 0 , 2 ) .
MarginTop ( 1 ) .
MarginBottom ( 1 )
var b strings . Builder
b . WriteString ( titleStyle . Render ( "🚇 Tunnel Pls" ) )
b . WriteString ( "\n" )
b . WriteString ( subtitleStyle . Render ( "Project by Bagas" ) )
b . WriteString ( "\n" )
b . WriteString ( urlStyle . Render ( "https://fossy.my.id" ) )
b . WriteString ( "\n\n" )
b . WriteString ( sectionTitleStyle . Render ( "Welcome to Tunnel!" ) )
b . WriteString ( "\n" )
b . WriteString ( "\n" )
forwardingText := fmt . Sprintf ( "🌐 Forwarding your traffic to:\n %s" , m . tunnelURL )
b . WriteString ( forwardingStyle . Render ( forwardingText ) )
b . WriteString ( m . helpView ( ) )
return b . String ( )
}
func ( i * Interaction ) Start ( ) {
lipgloss . SetColorProfile ( termenv . TrueColor )
domain := utils . Getenv ( "DOMAIN" , "localhost" )
protocol := "http"
if utils . Getenv ( "TLS_ENABLED" , "false" ) == "true" {
protocol = "https"
}
tunnelType := i . forwarder . GetTunnelType ( )
port := i . forwarder . GetForwardedPort ( )
var tunnelURL string
if tunnelType == types . HTTP {
tunnelURL = buildURL ( protocol , i . slugManager . Get ( ) , domain )
} else {
tunnelURL = fmt . Sprintf ( "tcp://%s:%d" , domain , port )
}
items := [ ] list . Item {
commandItem { name : "slug" , desc : "Set custom subdomain" } ,
commandItem { name : "tunnel-type" , desc : "Change tunnel type (Coming Soon)" } ,
}
delegate := list . NewDefaultDelegate ( )
delegate . ShowDescription = true
delegate . SetHeight ( 2 )
commandList := list . New ( items , delegate , 80 , 20 )
commandList . Title = "Select a command"
commandList . SetShowStatusBar ( false )
commandList . SetFilteringEnabled ( false )
commandList . SetShowHelp ( false )
ti := textinput . New ( )
ti . Placeholder = "my-custom-slug"
ti . CharLimit = 20
ti . Width = 50
m := model {
tunnelURL : tunnelURL ,
domain : domain ,
protocol : protocol ,
tunnelType : tunnelType ,
port : port ,
commandList : commandList ,
slugInput : ti ,
interaction : i ,
keymap : keymap {
quit : key . NewBinding (
key . WithKeys ( "q" , "ctrl+c" ) ,
key . WithHelp ( "q" , "quit" ) ,
) ,
command : key . NewBinding (
key . WithKeys ( "c" ) ,
key . WithHelp ( "c" , "commands" ) ,
) ,
random : key . NewBinding (
key . WithKeys ( "ctrl+r" ) ,
key . WithHelp ( "ctrl+r" , "random" ) ,
) ,
} ,
help : help . New ( ) ,
}
i . program = tea . NewProgram (
m ,
tea . WithInput ( i . channel ) ,
tea . WithOutput ( i . channel ) ,
tea . WithAltScreen ( ) ,
tea . WithMouseCellMotion ( ) ,
tea . WithoutSignals ( ) ,
tea . WithoutSignalHandler ( ) ,
)
_ , err := i . program . Run ( )
if err != nil {
log . Printf ( "Cannot close tea: %s \n" , err )
}
i . program . Kill ( )
i . program = nil
if err := m . interaction . lifecycle . Close ( ) ; err != nil {
log . Printf ( "Cannot close session: %s \n" , err )
}
}
func buildURL ( protocol , subdomain , domain string ) string {
return fmt . Sprintf ( "%s://%s.%s" , protocol , subdomain , domain )
}
func generateRandomSubdomain ( ) string {
return utils . GenerateRandomString ( 20 )
}