- Display authenticated username in welcome page information box - Refactor handleConnection function for better structure and clarity
837 lines
21 KiB
Go
837 lines
21 KiB
Go
package interaction
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
"tunnel_pls/internal/config"
|
|
"tunnel_pls/internal/random"
|
|
"tunnel_pls/session/slug"
|
|
"tunnel_pls/types"
|
|
|
|
"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"
|
|
)
|
|
|
|
type Lifecycle interface {
|
|
Close() error
|
|
GetUser() string
|
|
}
|
|
|
|
type SessionRegistry interface {
|
|
Update(user string, oldKey, newKey types.SessionKey) error
|
|
}
|
|
|
|
type Controller interface {
|
|
SetChannel(channel ssh.Channel)
|
|
SetLifecycle(lifecycle Lifecycle)
|
|
Start()
|
|
SetWH(w, h int)
|
|
Redraw()
|
|
SetSessionRegistry(registry SessionRegistry)
|
|
}
|
|
|
|
type Forwarder interface {
|
|
Close() error
|
|
GetTunnelType() types.TunnelType
|
|
GetForwardedPort() uint16
|
|
}
|
|
|
|
type Interaction struct {
|
|
channel ssh.Channel
|
|
slugManager slug.Manager
|
|
forwarder Forwarder
|
|
lifecycle Lifecycle
|
|
sessionRegistry SessionRegistry
|
|
program *tea.Program
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
func (i *Interaction) SetWH(w, h int) {
|
|
if i.program != nil {
|
|
i.program.Send(tea.WindowSizeMsg{
|
|
Width: w,
|
|
Height: h,
|
|
})
|
|
}
|
|
}
|
|
|
|
type commandItem struct {
|
|
name string
|
|
desc string
|
|
}
|
|
|
|
type model struct {
|
|
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
|
|
width int
|
|
height int
|
|
}
|
|
|
|
func (m *model) getTunnelURL() string {
|
|
if m.tunnelType == types.HTTP {
|
|
return buildURL(m.protocol, m.interaction.slugManager.Get(), m.domain)
|
|
}
|
|
return fmt.Sprintf("tcp://%s:%d", m.domain, m.port)
|
|
}
|
|
|
|
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{
|
|
channel: nil,
|
|
slugManager: slugManager,
|
|
forwarder: forwarder,
|
|
lifecycle: nil,
|
|
sessionRegistry: nil,
|
|
program: nil,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (i *Interaction) SetSessionRegistry(registry SessionRegistry) {
|
|
i.sessionRegistry = registry
|
|
}
|
|
|
|
func (i *Interaction) SetLifecycle(lifecycle Lifecycle) {
|
|
i.lifecycle = lifecycle
|
|
}
|
|
|
|
func (i *Interaction) SetChannel(channel ssh.Channel) {
|
|
i.channel = channel
|
|
}
|
|
|
|
func (i *Interaction) Stop() {
|
|
if i.cancel != nil {
|
|
i.cancel()
|
|
}
|
|
if i.program != nil {
|
|
i.program.Kill()
|
|
i.program = nil
|
|
}
|
|
}
|
|
|
|
func getResponsiveWidth(screenWidth, padding, minWidth, maxWidth int) int {
|
|
width := screenWidth - padding
|
|
if width > maxWidth {
|
|
width = maxWidth
|
|
}
|
|
if width < minWidth {
|
|
width = minWidth
|
|
}
|
|
return width
|
|
}
|
|
|
|
func shouldUseCompactLayout(width int, threshold int) bool {
|
|
return width < threshold
|
|
}
|
|
|
|
func truncateString(s string, maxLength int) string {
|
|
if len(s) <= maxLength {
|
|
return s
|
|
}
|
|
if maxLength < 4 {
|
|
return s[:maxLength]
|
|
}
|
|
return s[:maxLength-3] + "..."
|
|
}
|
|
|
|
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.width = msg.Width
|
|
m.height = msg.Height
|
|
m.commandList.SetWidth(msg.Width)
|
|
m.commandList.SetHeight(msg.Height - 4)
|
|
|
|
if msg.Width < 80 {
|
|
m.slugInput.Width = msg.Width - 10
|
|
} else {
|
|
m.slugInput.Width = 50
|
|
}
|
|
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 m.editingSlug {
|
|
if m.tunnelType != types.HTTP {
|
|
m.editingSlug = false
|
|
m.slugError = ""
|
|
return m, tea.Batch(tea.ClearScreen, textinput.Blink)
|
|
}
|
|
switch msg.String() {
|
|
case "esc":
|
|
m.editingSlug = false
|
|
m.slugError = ""
|
|
return m, tea.Batch(tea.ClearScreen, textinput.Blink)
|
|
case "enter":
|
|
inputValue := m.slugInput.Value()
|
|
if err := m.interaction.sessionRegistry.Update(m.interaction.lifecycle.GetUser(), types.SessionKey{
|
|
Id: m.interaction.slugManager.Get(),
|
|
Type: types.HTTP,
|
|
}, types.SessionKey{
|
|
Id: inputValue,
|
|
Type: types.HTTP,
|
|
}); err != nil {
|
|
m.slugError = err.Error()
|
|
return m, nil
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
switch {
|
|
case key.Matches(msg, m.keymap.quit):
|
|
m.quitting = 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 m, nil
|
|
}
|
|
|
|
func (i *Interaction) Redraw() {
|
|
if i.program != nil {
|
|
i.program.Send(tea.ClearScreen())
|
|
}
|
|
}
|
|
|
|
func (m *model) helpView() string {
|
|
return "\n" + m.help.ShortHelpView([]key.Binding{
|
|
m.keymap.command,
|
|
m.keymap.quit,
|
|
})
|
|
}
|
|
|
|
func (m *model) View() string {
|
|
if m.quitting {
|
|
return ""
|
|
}
|
|
|
|
if m.showingComingSoon {
|
|
isCompact := shouldUseCompactLayout(m.width, 60)
|
|
|
|
var boxPadding int
|
|
var boxMargin int
|
|
if isCompact {
|
|
boxPadding = 1
|
|
boxMargin = 1
|
|
} else {
|
|
boxPadding = 3
|
|
boxMargin = 2
|
|
}
|
|
|
|
titleStyle := lipgloss.NewStyle().
|
|
Bold(true).
|
|
Foreground(lipgloss.Color("#7D56F4")).
|
|
PaddingTop(1).
|
|
PaddingBottom(1)
|
|
|
|
messageBoxWidth := getResponsiveWidth(m.width, 10, 30, 60)
|
|
messageBoxStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#FAFAFA")).
|
|
Background(lipgloss.Color("#1A1A2E")).
|
|
Bold(true).
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(lipgloss.Color("#7D56F4")).
|
|
Padding(1, boxPadding).
|
|
MarginTop(boxMargin).
|
|
MarginBottom(boxMargin).
|
|
Width(messageBoxWidth).
|
|
Align(lipgloss.Center)
|
|
|
|
helpStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#666666")).
|
|
Italic(true).
|
|
MarginTop(1)
|
|
|
|
var b strings.Builder
|
|
b.WriteString("\n\n")
|
|
|
|
var title string
|
|
if shouldUseCompactLayout(m.width, 40) {
|
|
title = "Coming Soon"
|
|
} else {
|
|
title = "⏳ Coming Soon"
|
|
}
|
|
b.WriteString(titleStyle.Render(title))
|
|
b.WriteString("\n\n")
|
|
|
|
var message string
|
|
if shouldUseCompactLayout(m.width, 50) {
|
|
message = "Coming soon!\nStay tuned."
|
|
} else {
|
|
message = "🚀 This feature is coming very soon!\n Stay tuned for updates."
|
|
}
|
|
b.WriteString(messageBoxStyle.Render(message))
|
|
b.WriteString("\n\n")
|
|
|
|
var helpText string
|
|
if shouldUseCompactLayout(m.width, 60) {
|
|
helpText = "Press any key..."
|
|
} else {
|
|
helpText = "This message will disappear in 5 seconds or press any key..."
|
|
}
|
|
b.WriteString(helpStyle.Render(helpText))
|
|
|
|
return b.String()
|
|
}
|
|
|
|
if m.editingSlug {
|
|
isCompact := shouldUseCompactLayout(m.width, 70)
|
|
isVeryCompact := shouldUseCompactLayout(m.width, 50)
|
|
|
|
var boxPadding int
|
|
var boxMargin int
|
|
if isVeryCompact {
|
|
boxPadding = 1
|
|
boxMargin = 1
|
|
} else if isCompact {
|
|
boxPadding = 1
|
|
boxMargin = 1
|
|
} else {
|
|
boxPadding = 2
|
|
boxMargin = 2
|
|
}
|
|
|
|
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, boxPadding).
|
|
MarginTop(boxMargin).
|
|
MarginBottom(boxMargin)
|
|
|
|
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, boxPadding).
|
|
MarginTop(1).
|
|
MarginBottom(1)
|
|
|
|
rulesBoxWidth := getResponsiveWidth(m.width, 10, 30, 60)
|
|
rulesBoxStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#FAFAFA")).
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(lipgloss.Color("#7D56F4")).
|
|
Padding(0, boxPadding).
|
|
MarginTop(1).
|
|
MarginBottom(1).
|
|
Width(rulesBoxWidth)
|
|
|
|
var b strings.Builder
|
|
var title string
|
|
if isVeryCompact {
|
|
title = "Edit Subdomain"
|
|
} else {
|
|
title = "🔧 Edit Subdomain"
|
|
}
|
|
b.WriteString(titleStyle.Render(title))
|
|
b.WriteString("\n\n")
|
|
|
|
if m.tunnelType != types.HTTP {
|
|
warningBoxWidth := getResponsiveWidth(m.width, 10, 30, 60)
|
|
warningBoxStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#FFA500")).
|
|
Background(lipgloss.Color("#3D2000")).
|
|
Bold(true).
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(lipgloss.Color("#FFA500")).
|
|
Padding(1, boxPadding).
|
|
MarginTop(boxMargin).
|
|
MarginBottom(boxMargin).
|
|
Width(warningBoxWidth)
|
|
|
|
var warningText string
|
|
if isVeryCompact {
|
|
warningText = "⚠️ TCP tunnels don't support custom subdomains."
|
|
} else {
|
|
warningText = "⚠️ TCP tunnels cannot have custom subdomains. Only HTTP/HTTPS tunnels support subdomain customization."
|
|
}
|
|
b.WriteString(warningBoxStyle.Render(warningText))
|
|
b.WriteString("\n\n")
|
|
|
|
var helpText string
|
|
if isVeryCompact {
|
|
helpText = "Press any key to go back"
|
|
} else {
|
|
helpText = "Press Enter or Esc to go back"
|
|
}
|
|
b.WriteString(helpStyle.Render(helpText))
|
|
return b.String()
|
|
}
|
|
|
|
var rulesContent string
|
|
if isVeryCompact {
|
|
rulesContent = "Rules:\n3-20 chars\na-z, 0-9, -\nNo leading/trailing -"
|
|
} else if isCompact {
|
|
rulesContent = "📋 Rules:\n • 3-20 chars\n • a-z, 0-9, -\n • No leading/trailing -"
|
|
} else {
|
|
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")
|
|
|
|
var instruction string
|
|
if isVeryCompact {
|
|
instruction = "Custom subdomain:"
|
|
} else {
|
|
instruction = "Enter your custom subdomain:"
|
|
}
|
|
b.WriteString(instructionStyle.Render(instruction))
|
|
b.WriteString("\n")
|
|
|
|
if m.slugError != "" {
|
|
errorInputBoxStyle := lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(lipgloss.Color("#FF0000")).
|
|
Padding(1, boxPadding).
|
|
MarginTop(boxMargin).
|
|
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)
|
|
previewWidth := getResponsiveWidth(m.width, 10, 30, 80)
|
|
|
|
if len(previewURL) > previewWidth-10 {
|
|
previewURL = truncateString(previewURL, previewWidth-10)
|
|
}
|
|
|
|
previewStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#04B575")).
|
|
Italic(true).
|
|
Width(previewWidth)
|
|
b.WriteString(previewStyle.Render(fmt.Sprintf("Preview: %s", previewURL)))
|
|
b.WriteString("\n")
|
|
|
|
var helpText string
|
|
if isVeryCompact {
|
|
helpText = "Enter: save • CTRL+R: random • Esc: cancel"
|
|
} else {
|
|
helpText = "Press Enter to save • CTRL+R for random • Esc to cancel"
|
|
}
|
|
b.WriteString(helpStyle.Render(helpText))
|
|
|
|
return b.String()
|
|
}
|
|
|
|
if m.showingCommands {
|
|
isCompact := shouldUseCompactLayout(m.width, 60)
|
|
|
|
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")
|
|
|
|
var title string
|
|
if shouldUseCompactLayout(m.width, 40) {
|
|
title = "Commands"
|
|
} else {
|
|
title = "⚡ Commands"
|
|
}
|
|
b.WriteString(titleStyle.Render(title))
|
|
b.WriteString("\n\n")
|
|
b.WriteString(m.commandList.View())
|
|
b.WriteString("\n")
|
|
|
|
var helpText string
|
|
if isCompact {
|
|
helpText = "↑/↓ Nav • Enter Select • Esc Cancel"
|
|
} else {
|
|
helpText = "↑/↓ Navigate • Enter Select • Esc Cancel"
|
|
}
|
|
b.WriteString(helpStyle.Render(helpText))
|
|
|
|
return b.String()
|
|
}
|
|
|
|
titleStyle := lipgloss.NewStyle().
|
|
Bold(true).
|
|
Foreground(lipgloss.Color("#7D56F4")).
|
|
PaddingTop(1)
|
|
|
|
subtitleStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#888888")).
|
|
Italic(true)
|
|
|
|
urlStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#7D56F4")).
|
|
Underline(true).
|
|
Italic(true)
|
|
|
|
urlBoxStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#04B575")).
|
|
Bold(true).
|
|
Italic(true)
|
|
|
|
keyHintStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#7D56F4")).
|
|
Bold(true)
|
|
|
|
var b strings.Builder
|
|
|
|
isCompact := shouldUseCompactLayout(m.width, 85)
|
|
|
|
var asciiArtMargin int
|
|
if isCompact {
|
|
asciiArtMargin = 0
|
|
} else {
|
|
asciiArtMargin = 1
|
|
}
|
|
|
|
asciiArtStyle := lipgloss.NewStyle().
|
|
Bold(true).
|
|
Foreground(lipgloss.Color("#7D56F4")).
|
|
MarginBottom(asciiArtMargin)
|
|
|
|
var asciiArt string
|
|
if shouldUseCompactLayout(m.width, 50) {
|
|
asciiArt = "TUNNEL PLS"
|
|
} else if isCompact {
|
|
asciiArt = `
|
|
▀█▀ █ █ █▄ █ █▄ █ ██▀ █ ▄▀▀ █ ▄▀▀
|
|
█ ▀▄█ █ ▀█ █ ▀█ █▄▄ █▄▄ ▄█▀ █▄▄ ▄█▀`
|
|
} else {
|
|
asciiArt = `
|
|
████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗ ██████╗ ██╗ ███████╗
|
|
╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║ ██╔══██╗██║ ██╔════╝
|
|
██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██║ ██████╔╝██║ ███████╗
|
|
██║ ██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██║ ██╔═══╝ ██║ ╚════██║
|
|
██║ ╚██████╔╝██║ ╚████║██║ ╚████║███████╗███████╗ ██║ ███████╗███████║
|
|
╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝`
|
|
}
|
|
|
|
b.WriteString(asciiArtStyle.Render(asciiArt))
|
|
b.WriteString("\n")
|
|
|
|
if !shouldUseCompactLayout(m.width, 60) {
|
|
b.WriteString(subtitleStyle.Render("Secure tunnel service by Bagas • "))
|
|
b.WriteString(urlStyle.Render("https://fossy.my.id"))
|
|
b.WriteString("\n\n")
|
|
} else {
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
boxMaxWidth := getResponsiveWidth(m.width, 10, 40, 80)
|
|
var boxPadding int
|
|
var boxMargin int
|
|
if isCompact {
|
|
boxPadding = 1
|
|
boxMargin = 1
|
|
} else {
|
|
boxPadding = 2
|
|
boxMargin = 2
|
|
}
|
|
|
|
responsiveInfoBox := lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(lipgloss.Color("#7D56F4")).
|
|
Padding(1, boxPadding).
|
|
MarginTop(boxMargin).
|
|
MarginBottom(boxMargin).
|
|
Width(boxMaxWidth)
|
|
|
|
authenticatedUser := m.interaction.lifecycle.GetUser()
|
|
|
|
userInfoStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#FAFAFA")).
|
|
Bold(true)
|
|
|
|
sectionHeaderStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#888888")).
|
|
Bold(true)
|
|
|
|
addressStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#FAFAFA"))
|
|
|
|
var infoContent string
|
|
if shouldUseCompactLayout(m.width, 70) {
|
|
infoContent = fmt.Sprintf("👤 %s\n\n%s\n%s",
|
|
userInfoStyle.Render(authenticatedUser),
|
|
sectionHeaderStyle.Render("🌐 FORWARDING ADDRESS:"),
|
|
addressStyle.Render(fmt.Sprintf(" %s", urlBoxStyle.Render(m.getTunnelURL()))))
|
|
} else {
|
|
infoContent = fmt.Sprintf("👤 Authenticated as: %s\n\n%s\n %s",
|
|
userInfoStyle.Render(authenticatedUser),
|
|
sectionHeaderStyle.Render("🌐 FORWARDING ADDRESS:"),
|
|
addressStyle.Render(urlBoxStyle.Render(m.getTunnelURL())))
|
|
}
|
|
|
|
b.WriteString(responsiveInfoBox.Render(infoContent))
|
|
b.WriteString("\n")
|
|
|
|
var quickActionsTitle string
|
|
if shouldUseCompactLayout(m.width, 50) {
|
|
quickActionsTitle = "Actions"
|
|
} else if isCompact {
|
|
quickActionsTitle = "Quick Actions"
|
|
} else {
|
|
quickActionsTitle = "✨ Quick Actions"
|
|
}
|
|
b.WriteString(titleStyle.Render(quickActionsTitle))
|
|
b.WriteString("\n")
|
|
|
|
var featureMargin int
|
|
if isCompact {
|
|
featureMargin = 1
|
|
} else {
|
|
featureMargin = 2
|
|
}
|
|
|
|
compactFeatureStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#FAFAFA")).
|
|
MarginLeft(featureMargin)
|
|
|
|
var commandsText string
|
|
var quitText string
|
|
if shouldUseCompactLayout(m.width, 60) {
|
|
commandsText = fmt.Sprintf(" %s Commands", keyHintStyle.Render("[C]"))
|
|
quitText = fmt.Sprintf(" %s Quit", keyHintStyle.Render("[Q]"))
|
|
} else {
|
|
commandsText = fmt.Sprintf(" %s Open commands menu", keyHintStyle.Render("[C]"))
|
|
quitText = fmt.Sprintf(" %s Quit application", keyHintStyle.Render("[Q]"))
|
|
}
|
|
|
|
b.WriteString(compactFeatureStyle.Render(commandsText))
|
|
b.WriteString("\n")
|
|
b.WriteString(compactFeatureStyle.Render(quitText))
|
|
|
|
if !shouldUseCompactLayout(m.width, 70) {
|
|
b.WriteString("\n\n")
|
|
footerStyle := lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#666666")).
|
|
Italic(true)
|
|
b.WriteString(footerStyle.Render("Press 'C' to customize your tunnel settings"))
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (i *Interaction) Start() {
|
|
lipgloss.SetColorProfile(termenv.TrueColor)
|
|
|
|
domain := config.Getenv("DOMAIN", "localhost")
|
|
protocol := "http"
|
|
if config.Getenv("TLS_ENABLED", "false") == "true" {
|
|
protocol = "https"
|
|
}
|
|
|
|
tunnelType := i.forwarder.GetTunnelType()
|
|
port := i.forwarder.GetForwardedPort()
|
|
|
|
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{
|
|
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(),
|
|
tea.WithFPS(30),
|
|
)
|
|
|
|
_, 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 random.GenerateRandomString(20)
|
|
}
|