feat: make SSH interaction UI fully responsive
This commit is contained in:
@@ -19,7 +19,28 @@ var blockedReservedPorts = []uint16{1080, 1433, 1521, 1900, 2049, 3306, 3389, 54
|
|||||||
func (s *SSHSession) HandleGlobalRequest(GlobalRequest <-chan *ssh.Request) {
|
func (s *SSHSession) HandleGlobalRequest(GlobalRequest <-chan *ssh.Request) {
|
||||||
for req := range GlobalRequest {
|
for req := range GlobalRequest {
|
||||||
switch req.Type {
|
switch req.Type {
|
||||||
case "shell", "pty-req", "window-change":
|
case "shell", "pty-req":
|
||||||
|
err := req.Reply(true, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to reply to request:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "window-change":
|
||||||
|
p := req.Payload
|
||||||
|
if len(p) < 16 {
|
||||||
|
log.Println("invalid window-change payload")
|
||||||
|
err := req.Reply(false, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to reply to request:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cols := binary.BigEndian.Uint32(p[0:4])
|
||||||
|
rows := binary.BigEndian.Uint32(p[4:8])
|
||||||
|
|
||||||
|
s.interaction.SetWH(int(cols), int(rows))
|
||||||
|
|
||||||
err := req.Reply(true, nil)
|
err := req.Reply(true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to reply to request:", err)
|
log.Println("Failed to reply to request:", err)
|
||||||
@@ -104,25 +125,12 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
|
|||||||
if portToBind == 80 || portToBind == 443 {
|
if portToBind == 80 || portToBind == 443 {
|
||||||
s.HandleHTTPForward(req, portToBind)
|
s.HandleHTTPForward(req, portToBind)
|
||||||
return
|
return
|
||||||
} else {
|
}
|
||||||
if portToBind == 0 {
|
if portToBind == 0 {
|
||||||
unassign, success := portUtil.Default.GetUnassignedPort()
|
unassign, success := portUtil.Default.GetUnassignedPort()
|
||||||
portToBind = unassign
|
portToBind = unassign
|
||||||
if !success {
|
if !success {
|
||||||
log.Println("No available port")
|
log.Println("No available port")
|
||||||
err := req.Reply(false, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to reply to request:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = s.lifecycle.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to close session: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if isUse, isExist := portUtil.Default.GetPortStatus(portToBind); isExist && isUse {
|
|
||||||
log.Printf("Port %d is already in use or restricted", portToBind)
|
|
||||||
err := req.Reply(false, nil)
|
err := req.Reply(false, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to reply to request:", err)
|
log.Println("Failed to reply to request:", err)
|
||||||
@@ -134,12 +142,25 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := portUtil.Default.SetPortStatus(portToBind, true)
|
} else if isUse, isExist := portUtil.Default.GetPortStatus(portToBind); isExist && isUse {
|
||||||
|
log.Printf("Port %d is already in use or restricted", portToBind)
|
||||||
|
err := req.Reply(false, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to set port status:", err)
|
log.Println("Failed to reply to request:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
err = s.lifecycle.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to close session: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
err = portUtil.Default.SetPortStatus(portToBind, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to set port status:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s.HandleTCPForward(req, addr, portToBind)
|
s.HandleTCPForward(req, addr, portToBind)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,12 +196,6 @@ func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
|
|||||||
}
|
}
|
||||||
log.Printf("HTTP forwarding approved on port: %d", portToBind)
|
log.Printf("HTTP forwarding approved on port: %d", portToBind)
|
||||||
|
|
||||||
domain := utils.Getenv("DOMAIN", "localhost")
|
|
||||||
protocol := "http"
|
|
||||||
if utils.Getenv("TLS_ENABLED", "false") == "true" {
|
|
||||||
protocol = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
err = req.Reply(true, buf.Bytes())
|
err = req.Reply(true, buf.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to reply to request:", err)
|
log.Println("Failed to reply to request:", err)
|
||||||
@@ -195,7 +210,6 @@ func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
|
|||||||
s.forwarder.SetType(types.HTTP)
|
s.forwarder.SetType(types.HTTP)
|
||||||
s.forwarder.SetForwardedPort(portToBind)
|
s.forwarder.SetForwardedPort(portToBind)
|
||||||
s.slugManager.Set(slug)
|
s.slugManager.Set(slug)
|
||||||
log.Printf("HTTP tunnel established: %s://%s.%s", protocol, slug, domain)
|
|
||||||
s.lifecycle.SetStatus(types.RUNNING)
|
s.lifecycle.SetStatus(types.RUNNING)
|
||||||
s.interaction.Start()
|
s.interaction.Start()
|
||||||
}
|
}
|
||||||
@@ -253,7 +267,6 @@ func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind
|
|||||||
s.forwarder.SetType(types.TCP)
|
s.forwarder.SetType(types.TCP)
|
||||||
s.forwarder.SetListener(listener)
|
s.forwarder.SetListener(listener)
|
||||||
s.forwarder.SetForwardedPort(portToBind)
|
s.forwarder.SetForwardedPort(portToBind)
|
||||||
log.Printf("TCP tunnel established: tcp://%s:%d", utils.Getenv("DOMAIN", "localhost"), s.forwarder.GetForwardedPort())
|
|
||||||
s.lifecycle.SetStatus(types.RUNNING)
|
s.lifecycle.SetStatus(types.RUNNING)
|
||||||
go s.forwarder.AcceptTCPConnections()
|
go s.forwarder.AcceptTCPConnections()
|
||||||
s.interaction.Start()
|
s.interaction.Start()
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Controller interface {
|
|||||||
SetLifecycle(lifecycle Lifecycle)
|
SetLifecycle(lifecycle Lifecycle)
|
||||||
SetSlugModificator(func(oldSlug, newSlug string) bool)
|
SetSlugModificator(func(oldSlug, newSlug string) bool)
|
||||||
Start()
|
Start()
|
||||||
|
SetWH(w, h int)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Forwarder interface {
|
type Forwarder interface {
|
||||||
@@ -48,6 +49,15 @@ type Interaction struct {
|
|||||||
cancel context.CancelFunc
|
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 {
|
type commandItem struct {
|
||||||
name string
|
name string
|
||||||
desc string
|
desc string
|
||||||
@@ -69,6 +79,8 @@ type model struct {
|
|||||||
slugInput textinput.Model
|
slugInput textinput.Model
|
||||||
slugError string
|
slugError string
|
||||||
interaction *Interaction
|
interaction *Interaction
|
||||||
|
width int
|
||||||
|
height int
|
||||||
}
|
}
|
||||||
|
|
||||||
type keymap struct {
|
type keymap struct {
|
||||||
@@ -115,6 +127,31 @@ func (i *Interaction) Stop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) FilterValue() string { return i.name }
|
||||||
func (i commandItem) Title() string { return i.name }
|
func (i commandItem) Title() string { return i.name }
|
||||||
func (i commandItem) Description() string { return i.desc }
|
func (i commandItem) Description() string { return i.desc }
|
||||||
@@ -138,8 +175,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, tea.Batch(tea.ClearScreen, textinput.Blink)
|
return m, tea.Batch(tea.ClearScreen, textinput.Blink)
|
||||||
|
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
m.commandList.SetWidth(msg.Width)
|
m.commandList.SetWidth(msg.Width)
|
||||||
m.commandList.SetHeight(msg.Height - 4)
|
m.commandList.SetHeight(msg.Height - 4)
|
||||||
|
|
||||||
|
if msg.Width < 80 {
|
||||||
|
m.slugInput.Width = msg.Width - 10
|
||||||
|
} else {
|
||||||
|
m.slugInput.Width = 50
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.QuitMsg:
|
case tea.QuitMsg:
|
||||||
@@ -240,21 +285,35 @@ func (m model) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.showingComingSoon {
|
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().
|
titleStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#7D56F4")).
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
PaddingTop(1).
|
PaddingTop(1).
|
||||||
PaddingBottom(1)
|
PaddingBottom(1)
|
||||||
|
|
||||||
|
messageBoxWidth := getResponsiveWidth(m.width, 10, 30, 60)
|
||||||
messageBoxStyle := lipgloss.NewStyle().
|
messageBoxStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#FAFAFA")).
|
Foreground(lipgloss.Color("#FAFAFA")).
|
||||||
Background(lipgloss.Color("#1A1A2E")).
|
Background(lipgloss.Color("#1A1A2E")).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#7D56F4")).
|
BorderForeground(lipgloss.Color("#7D56F4")).
|
||||||
Padding(1, 3).
|
Padding(1, boxPadding).
|
||||||
MarginTop(2).
|
MarginTop(boxMargin).
|
||||||
MarginBottom(2).
|
MarginBottom(boxMargin).
|
||||||
|
Width(messageBoxWidth).
|
||||||
Align(lipgloss.Center)
|
Align(lipgloss.Center)
|
||||||
|
|
||||||
helpStyle := lipgloss.NewStyle().
|
helpStyle := lipgloss.NewStyle().
|
||||||
@@ -264,16 +323,53 @@ func (m model) View() string {
|
|||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(titleStyle.Render("⏳ Coming Soon"))
|
|
||||||
|
var title string
|
||||||
|
if shouldUseCompactLayout(m.width, 40) {
|
||||||
|
title = "Coming Soon"
|
||||||
|
} else {
|
||||||
|
title = "⏳ Coming Soon"
|
||||||
|
}
|
||||||
|
b.WriteString(titleStyle.Render(title))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(messageBoxStyle.Render("🚀 This feature is coming very soon!\n Stay tuned for updates."))
|
|
||||||
|
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")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(helpStyle.Render("This message will disappear in 5 seconds or press any key..."))
|
|
||||||
|
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()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.editingSlug {
|
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().
|
titleStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#7D56F4")).
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
@@ -287,9 +383,9 @@ func (m model) View() string {
|
|||||||
inputBoxStyle := lipgloss.NewStyle().
|
inputBoxStyle := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#7D56F4")).
|
BorderForeground(lipgloss.Color("#7D56F4")).
|
||||||
Padding(1, 2).
|
Padding(1, boxPadding).
|
||||||
MarginTop(2).
|
MarginTop(boxMargin).
|
||||||
MarginBottom(2)
|
MarginBottom(boxMargin)
|
||||||
|
|
||||||
helpStyle := lipgloss.NewStyle().
|
helpStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#666666")).
|
Foreground(lipgloss.Color("#666666")).
|
||||||
@@ -302,52 +398,88 @@ func (m model) View() string {
|
|||||||
Bold(true).
|
Bold(true).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#FF0000")).
|
BorderForeground(lipgloss.Color("#FF0000")).
|
||||||
Padding(0, 2).
|
Padding(0, boxPadding).
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
MarginBottom(1)
|
MarginBottom(1)
|
||||||
|
|
||||||
|
rulesBoxWidth := getResponsiveWidth(m.width, 10, 30, 60)
|
||||||
rulesBoxStyle := lipgloss.NewStyle().
|
rulesBoxStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#FAFAFA")).
|
Foreground(lipgloss.Color("#FAFAFA")).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#7D56F4")).
|
BorderForeground(lipgloss.Color("#7D56F4")).
|
||||||
Padding(0, 2).
|
Padding(0, boxPadding).
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
MarginBottom(1)
|
MarginBottom(1).
|
||||||
|
Width(rulesBoxWidth)
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString(titleStyle.Render("🔧 Edit Subdomain"))
|
var title string
|
||||||
|
if isVeryCompact {
|
||||||
|
title = "Edit Subdomain"
|
||||||
|
} else {
|
||||||
|
title = "🔧 Edit Subdomain"
|
||||||
|
}
|
||||||
|
b.WriteString(titleStyle.Render(title))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
if m.tunnelType != types.HTTP {
|
if m.tunnelType != types.HTTP {
|
||||||
|
warningBoxWidth := getResponsiveWidth(m.width, 10, 30, 60)
|
||||||
warningBoxStyle := lipgloss.NewStyle().
|
warningBoxStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#FFA500")).
|
Foreground(lipgloss.Color("#FFA500")).
|
||||||
Background(lipgloss.Color("#3D2000")).
|
Background(lipgloss.Color("#3D2000")).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#FFA500")).
|
BorderForeground(lipgloss.Color("#FFA500")).
|
||||||
Padding(1, 2).
|
Padding(1, boxPadding).
|
||||||
MarginTop(2).
|
MarginTop(boxMargin).
|
||||||
MarginBottom(2)
|
MarginBottom(boxMargin).
|
||||||
|
Width(warningBoxWidth)
|
||||||
|
|
||||||
b.WriteString(warningBoxStyle.Render("⚠️ TCP tunnels cannot have custom subdomains. Only HTTP/HTTPS tunnels support subdomain customization. "))
|
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")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(helpStyle.Render("Press Enter or Esc to go back"))
|
|
||||||
|
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()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
rulesContent := "📋 Rules: \n\t• 3-20 chars \n\t• a-z, 0-9, - \n\t• No leading/trailing -"
|
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(rulesBoxStyle.Render(rulesContent))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
b.WriteString(instructionStyle.Render("Enter your custom subdomain:"))
|
var instruction string
|
||||||
|
if isVeryCompact {
|
||||||
|
instruction = "Custom subdomain:"
|
||||||
|
} else {
|
||||||
|
instruction = "Enter your custom subdomain:"
|
||||||
|
}
|
||||||
|
b.WriteString(instructionStyle.Render(instruction))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
if m.slugError != "" {
|
if m.slugError != "" {
|
||||||
errorInputBoxStyle := lipgloss.NewStyle().
|
errorInputBoxStyle := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#FF0000")).
|
BorderForeground(lipgloss.Color("#FF0000")).
|
||||||
Padding(1, 2).
|
Padding(1, boxPadding).
|
||||||
MarginTop(2).
|
MarginTop(boxMargin).
|
||||||
MarginBottom(1)
|
MarginBottom(1)
|
||||||
b.WriteString(errorInputBoxStyle.Render(m.slugInput.View()))
|
b.WriteString(errorInputBoxStyle.Render(m.slugInput.View()))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -359,19 +491,33 @@ func (m model) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
previewURL := buildURL(m.protocol, m.slugInput.Value(), m.domain)
|
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().
|
previewStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#04B575")).
|
Foreground(lipgloss.Color("#04B575")).
|
||||||
Italic(true).
|
Italic(true).
|
||||||
Width(80)
|
Width(previewWidth)
|
||||||
b.WriteString(previewStyle.Render(fmt.Sprintf("Preview: %s", previewURL)))
|
b.WriteString(previewStyle.Render(fmt.Sprintf("Preview: %s", previewURL)))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
b.WriteString(helpStyle.Render("Press Enter to save • CTRL+R for random • Esc to cancel"))
|
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()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.showingCommands {
|
if m.showingCommands {
|
||||||
|
isCompact := shouldUseCompactLayout(m.width, 60)
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
titleStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#7D56F4")).
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
@@ -385,11 +531,25 @@ func (m model) View() string {
|
|||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(titleStyle.Render("⚡ Commands"))
|
|
||||||
|
var title string
|
||||||
|
if shouldUseCompactLayout(m.width, 40) {
|
||||||
|
title = "Commands"
|
||||||
|
} else {
|
||||||
|
title = "⚡ Commands"
|
||||||
|
}
|
||||||
|
b.WriteString(titleStyle.Render(title))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(m.commandList.View())
|
b.WriteString(m.commandList.View())
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(helpStyle.Render("↑/↓ Navigate • Enter Select • Esc Cancel"))
|
|
||||||
|
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()
|
return b.String()
|
||||||
}
|
}
|
||||||
@@ -397,49 +557,151 @@ func (m model) View() string {
|
|||||||
titleStyle := lipgloss.NewStyle().
|
titleStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#7D56F4")).
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
PaddingTop(1).
|
PaddingTop(1)
|
||||||
PaddingBottom(1)
|
|
||||||
|
|
||||||
subtitleStyle := lipgloss.NewStyle().
|
subtitleStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#7D56F4")).
|
Foreground(lipgloss.Color("#888888")).
|
||||||
Italic(true)
|
Italic(true)
|
||||||
|
|
||||||
urlStyle := lipgloss.NewStyle().
|
urlStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#04B575")).
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
Underline(true)
|
Underline(true).
|
||||||
|
Italic(true)
|
||||||
|
|
||||||
sectionTitleStyle := lipgloss.NewStyle().
|
urlBoxStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#FAFAFA")).
|
|
||||||
MarginTop(1).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
forwardingStyle := lipgloss.NewStyle().
|
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#04B575")).
|
Foreground(lipgloss.Color("#04B575")).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Bold(true).
|
||||||
BorderForeground(lipgloss.Color("#04B575")).
|
Italic(true)
|
||||||
Padding(0, 2).
|
|
||||||
MarginTop(1).
|
keyHintStyle := lipgloss.NewStyle().
|
||||||
MarginBottom(1)
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
b.WriteString(titleStyle.Render("🚇 Tunnel Pls"))
|
isCompact := shouldUseCompactLayout(m.width, 85)
|
||||||
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!"))
|
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")
|
b.WriteString("\n")
|
||||||
|
|
||||||
b.WriteString("\n")
|
if !shouldUseCompactLayout(m.width, 60) {
|
||||||
forwardingText := fmt.Sprintf("🌐 Forwarding your traffic to:\n %s", m.tunnelURL)
|
b.WriteString(subtitleStyle.Render("Secure tunnel service by Bagas • "))
|
||||||
b.WriteString(forwardingStyle.Render(forwardingText))
|
b.WriteString(urlStyle.Render("https://fossy.my.id"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
b.WriteString(m.helpView())
|
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)
|
||||||
|
|
||||||
|
urlDisplay := m.tunnelURL
|
||||||
|
if shouldUseCompactLayout(m.width, 80) && len(m.tunnelURL) > m.width-20 {
|
||||||
|
maxLen := m.width - 25
|
||||||
|
if maxLen > 10 {
|
||||||
|
urlDisplay = truncateString(m.tunnelURL, maxLen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var infoContent string
|
||||||
|
if shouldUseCompactLayout(m.width, 70) {
|
||||||
|
infoContent = fmt.Sprintf("🌐 %s", urlBoxStyle.Render(urlDisplay))
|
||||||
|
} else if isCompact {
|
||||||
|
infoContent = fmt.Sprintf("🌐 Forwarding to:\n\n %s", urlBoxStyle.Render(urlDisplay))
|
||||||
|
} else {
|
||||||
|
infoContent = fmt.Sprintf("🌐 F O R W A R D I N G T O:\n\n %s", urlBoxStyle.Render(urlDisplay))
|
||||||
|
}
|
||||||
|
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()
|
return b.String()
|
||||||
}
|
}
|
||||||
@@ -517,6 +779,7 @@ func (i *Interaction) Start() {
|
|||||||
tea.WithMouseCellMotion(),
|
tea.WithMouseCellMotion(),
|
||||||
tea.WithoutSignals(),
|
tea.WithoutSignals(),
|
||||||
tea.WithoutSignalHandler(),
|
tea.WithoutSignalHandler(),
|
||||||
|
tea.WithFPS(30),
|
||||||
)
|
)
|
||||||
|
|
||||||
_, err := i.program.Run()
|
_, err := i.program.Run()
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ func New(conn *ssh.ServerConn, forwardingReq <-chan *ssh.Request, sshChan <-chan
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session.HandleTCPIPForward(tcpipReq)
|
go session.HandleTCPIPForward(tcpipReq)
|
||||||
})
|
})
|
||||||
go session.HandleGlobalRequest(reqs)
|
session.HandleGlobalRequest(reqs)
|
||||||
}
|
}
|
||||||
if err := session.lifecycle.Close(); err != nil {
|
if err := session.lifecycle.Close(); err != nil {
|
||||||
log.Printf("failed to close session: %v", err)
|
log.Printf("failed to close session: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user