Skip to content
Merged
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
4 changes: 2 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];

security.wrappers.justrayd = lib.mkIf pkgs.stdenv.isLinux {
security.wrappers.justrayd = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
source = "${cfg.package}/bin/justrayd";
capabilities = "cap_net_admin+ep";
owner = "root";
Expand Down Expand Up @@ -148,7 +148,7 @@
config = lib.mkIf cfg.enable {
home.packages = [ cfg.package ];

systemd.user.services.justrayd = lib.mkIf pkgs.stdenv.isLinux {
systemd.user.services.justrayd = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
Unit = {
After = [ "network-online.target" ];
Wants = [ "network-online.target" ];
Expand Down
8 changes: 3 additions & 5 deletions internal/client/cli/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ func out(s string) { _, _ = lipgloss.Println(s) }

func done(text string) { out(style.Alive.Bold(true).Render("✓") + " " + text) }

func fields(pairs ...[2]string) { out(fieldLines(pairs...)) }

func fieldLines(pairs ...[2]string) string {
func fields(pairs ...[2]string) {
w := 0
for _, p := range pairs {
w = max(w, lipgloss.Width(p[0]))
Expand All @@ -31,13 +29,13 @@ func fieldLines(pairs ...[2]string) string {
for i, p := range pairs {
lines[i] = " " + style.Pad(style.Dim.Render(p[0]+":"), w+1) + " " + p[1]
}
return strings.Join(lines, "\n")
out(strings.Join(lines, "\n"))
}

func state(st ipc.Status) string {
if st.Connected {
text := "connected via " + strings.ToUpper(modeWord(st.Tun))
if st.Uptime >= 0 {
if st.Uptime > 0 {
text += " for " + style.Uptime(time.Duration(st.Uptime)*time.Second)
}
return text
Expand Down
13 changes: 9 additions & 4 deletions internal/client/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func init() {
rootCmd.SetUsageTemplate(usageTemplate)
rootCmd.SetVersionTemplate("{{versionBlock}}")
rootCmd.AddGroup(&cobra.Group{ID: cmdGroup, Title: "AVAILABLE COMMANDS"})
rootCmd.AddCommand(upCmd, downCmd, stopCmd, statusCmd, subCmd)
rootCmd.AddCommand(upCmd, downCmd, stopCmd, statusCmd, subCmd, versionCmd)
}

// Execute runs the justray CLI. The caller (cmd/justray) handles the error.
Expand All @@ -80,7 +80,7 @@ func Execute() error {
rootCmd.Use = filepath.Base(os.Args[0]) + " <command>"
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
for c := cmd; c != nil; c = c.Parent() {
if c.Name() == "completion" || c.Name() == "help" || c.Name() == "stop" {
if c.Name() == "completion" || c.Name() == "help" || c.Name() == "stop" || c.Name() == "version" {
return nil
}
}
Expand Down Expand Up @@ -181,13 +181,18 @@ func spawn(dir string) error {
}

func justrayd() (string, error) {
bin, err := exec.LookPath(exeName("justrayd"))
if err == nil {
if bin, err := exec.LookPath(exeName("justrayd")); err == nil {
return bin, nil
}
if bin := nextToSelf("justrayd"); bin != "" {
return bin, nil
}
if dir, err := ipc.Dir(); err == nil {
p := filepath.Join(dir, "elevated", exeName("justrayd"))
if _, err := os.Stat(p); err == nil {
return p, nil
}
}
return "", fmt.Errorf("daemon not in PATH or next to client")
}

Expand Down
22 changes: 20 additions & 2 deletions internal/client/cli/stop.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package cli

import "github.com/spf13/cobra"
import (
"errors"
"time"

"github.com/spf13/cobra"

"github.com/luynrs/justray/internal/ipc"
)

var stopCmd = &cobra.Command{
Use: "stop",
Expand All @@ -16,11 +23,22 @@ func (a *app) stop(cmd *cobra.Command, args []string) error {
return nil
}
stop := spin("Stopping daemon")
err := c.Shutdown()
_ = c.Shutdown()
err := waitStopped(c, 5*time.Second)
stop()
if err != nil {
return err
}
done("Daemon stopped")
return nil
}

func waitStopped(c *ipc.Client, timeout time.Duration) error {
for deadline := time.Now().Add(timeout); time.Now().Before(deadline); {
if c.Ping() != nil {
return nil
}
time.Sleep(50 * time.Millisecond)
}
return errors.New("timed out waiting for daemon to stop")
}
45 changes: 20 additions & 25 deletions internal/client/cli/version.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,32 @@
package cli

import (
"debug/buildinfo"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/spf13/cobra"

"github.com/luynrs/justray/internal/client/tui/style"
"github.com/luynrs/justray/internal/version"
)

func versionBlock() string {
var pairs [][2]string
if v := singboxVersion(); v != "" {
pairs = append(pairs, [2]string{"sing-box", v})
}
pairs = append(pairs, [2]string{"Platform", runtime.GOOS + "/" + runtime.GOARCH})

head := style.Dim.Render("·") + " JustRay " + style.Dim.Render(version.String())
return head + "\n" + fieldLines(pairs...) + "\n"
var versionCmd = &cobra.Command{
Use: "version",
Hidden: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
out(strings.TrimRight(versionBlock(), "\n"))
},
}

func singboxVersion() string {
bin, err := justrayd()
if err != nil {
return ""
}
info, err := buildinfo.ReadFile(bin)
if err != nil {
return ""
}
for _, d := range info.Deps {
if d.Path == "github.com/sagernet/sing-box" {
return d.Version
}
func versionBlock() string {
bin := "justray"
if len(os.Args) > 0 && os.Args[0] != "" {
bin = filepath.Base(os.Args[0])
}
return ""
ver := strings.TrimPrefix(version.String(), "v")
p := runtime.GOOS + "/" + runtime.GOARCH
return fmt.Sprintf("%s version %s (%s)\nhttps://github.com/luynrs/justray/releases/tag/%s\n", bin, ver, p, version.String())
}
28 changes: 7 additions & 21 deletions internal/client/tui/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
tea "charm.land/bubbletea/v2"

"github.com/luynrs/justray/internal/client/tui/tree"
"github.com/luynrs/justray/internal/domain"
"github.com/luynrs/justray/internal/ipc"
)

Expand All @@ -30,7 +29,7 @@ func (m Model) activate() (tea.Model, tea.Cmd) {
ref := r.Node.Ref()
act = func() (ipc.Snapshot, error) { return m.client.Connect(ref) }
}
return m, tea.Batch(m.spin.Tick, snapshotCmd("connect", act))
return m, snapshotCmd("connect", act)
}

func (m Model) collapse() (tea.Model, tea.Cmd) {
Expand Down Expand Up @@ -69,27 +68,19 @@ func (m Model) probe() (tea.Model, tea.Cmd) {
if !ok {
return m, nil
}
m.probing = map[domain.NodeRef]bool{}
if r.Kind == tree.Node {
m.probing[r.Node.Ref()] = true
if r.Node.Probing {
return m, nil
}
return m, snapshotCmd("probe", func() (ipc.Snapshot, error) { return m.client.Probe(r.Node.Sub, r.Node.ID) })
}
if r.Sub.ID == tree.Default {
return m, nil
}
for _, n := range m.nodes {
if n.Sub == r.Sub.ID {
m.probing[n.Ref()] = true
}
}
return m, snapshotCmd("probe", func() (ipc.Snapshot, error) { return m.client.Probe(r.Sub.ID, "") })
}

func (m Model) probeAll() (tea.Model, tea.Cmd) {
m.probing = map[domain.NodeRef]bool{}
for _, n := range m.nodes {
m.probing[n.Ref()] = true
}
return m, snapshotCmd("probe", func() (ipc.Snapshot, error) { return m.client.Probe("", "") })
}

Expand All @@ -102,16 +93,11 @@ func (m Model) refresh() (tea.Model, tea.Cmd) {
if id == tree.Default {
return m, nil
}
m.refreshing = map[string]bool{id: true}
return m, tea.Batch(m.spin.Tick, snapshotCmd("refresh", func() (ipc.Snapshot, error) { return m.client.Refresh(id) }))
return m, snapshotCmd("refresh", func() (ipc.Snapshot, error) { return m.client.Refresh(id) })
}

func (m Model) refreshAll() (tea.Model, tea.Cmd) {
m.refreshing = map[string]bool{}
for _, sub := range m.subs {
m.refreshing[sub.ID] = true
}
return m, tea.Batch(m.spin.Tick, snapshotCmd("refresh", m.client.RefreshAll))
return m, snapshotCmd("refresh", m.client.RefreshAll)
}

func (m Model) moveSub(dir int) (tea.Model, tea.Cmd) {
Expand All @@ -133,5 +119,5 @@ func (m Model) setTun(enable bool) (tea.Model, tea.Cmd) {
return m, nil
}
m.connecting = true
return m, tea.Batch(m.spin.Tick, snapshotCmd("connect", func() (ipc.Snapshot, error) { return m.client.SetTun(enable) }))
return m, snapshotCmd("connect", func() (ipc.Snapshot, error) { return m.client.SetTun(enable) })
}
43 changes: 19 additions & 24 deletions internal/client/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,21 @@ type Model struct {
subs []ipc.Sub
nodes []ipc.Node

collapsed map[string]bool
probing map[domain.NodeRef]bool
refreshing map[string]bool
spin spinner.Model
cursor int
scroll int
wheel time.Time

editor textinput.Model
confirmQ string
confirmID string
settings *settings.Settings
filter textinput.Model
collapsed map[string]bool
spin spinner.Model
cursor int
scroll int
wheel time.Time

editor textinput.Model
confirmSub ipc.Sub
dialog *settings.Settings
filter textinput.Model

status ipc.Status
revision uint64
live bool
emoji bool
cfg domain.Settings
since time.Time
statusCh chan pushed
watchCtx context.Context
Expand Down Expand Up @@ -83,16 +80,14 @@ func (m Model) Init() tea.Cmd {

func (m Model) data() tree.Data {
return tree.Data{
Subs: m.subs,
Nodes: m.nodes,
Collapsed: m.collapsed,
Probing: m.probing,
Refreshing: m.refreshing,
Query: m.filter.Value(),
Status: m.status,
Live: m.live,
Emoji: m.emoji,
Spinner: m.spin.View(),
Subs: m.subs,
Nodes: m.nodes,
Collapsed: m.collapsed,
Query: m.filter.Value(),
Status: m.status,
Live: m.live,
Emoji: m.cfg.Emoji == "on",
Spinner: m.spin.View(),
}
}

Expand Down
10 changes: 5 additions & 5 deletions internal/client/tui/settings/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ type hit struct {
}

func (s *Settings) lines(width, height int) []string {
w := max(width-4, 20)
s.input.SetWidth(max(w-8, 12))
w := max(width-2, 20)
s.input.SetWidth(max(w-6, 12))

rows := s.rows()
blocks := make([][]string, len(rows))
Expand Down Expand Up @@ -124,9 +124,9 @@ func tabAt(x int) (int, bool) {
func (s *Settings) fieldBlock(f field, i, width int) (lines, choices []string) {
selected := i == s.cursor

bar := " "
bar := " "
if selected {
bar = " " + style.Accent.Render("▎") + " "
bar = style.Accent.Render("▎ ")
}

switch {
Expand All @@ -145,7 +145,7 @@ func (s *Settings) fieldBlock(f field, i, width int) (lines, choices []string) {
lines = append(lines, value...)
choices = append(choices, picks...)
if selected && s.err != "" {
lines = append(lines, bar+style.Err.Render(style.Clip(style.FirstLine(s.err), width-4)))
lines = append(lines, bar+style.Err.Render(style.Clip(style.FirstLine(s.err), width-2)))
choices = append(choices, "")
}
}
Expand Down
4 changes: 2 additions & 2 deletions internal/client/tui/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,14 +386,14 @@ func (s *Settings) listRows(l list) []field {
set := func(v *domain.Settings, in string) error {
at := l.at(v)
if in = strings.TrimSpace(in); in == "" {
*at = append((*at)[:i:i], (*at)[i+1:]...)
*at = slices.Delete(*at, i, i+1)
return nil
}
rule, err := domain.ParseRule(in)
if err != nil {
return err
}
*at = append(append((*at)[:i:i], rule), (*at)[i+1:]...)
(*at)[i] = rule
return nil
}
out = append(out, field{
Expand Down
8 changes: 0 additions & 8 deletions internal/client/tui/style/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,6 @@ func Fit(body string, n int) string {
return strings.Join(lines[:max(n, 0)], "\n")
}

func Indent(s string) string {
var b strings.Builder
for line := range strings.Lines(s) {
b.WriteString(" " + line)
}
return b.String()
}

func FirstLine(s string) string {
line, _, _ := strings.Cut(s, "\n")
return line
Expand Down
Loading