Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
e0ac89e
docs(cmd)
TheRootDaemon Jul 20, 2026
6bdff05
tests(cmd)
TheRootDaemon Jul 20, 2026
7c3eb5a
tests(app): Add tests for search
TheRootDaemon Jul 20, 2026
db15e9b
tests(app): Add tests for info
TheRootDaemon Jul 20, 2026
de717d8
refactor(app): Simplify funcs, add tests
TheRootDaemon Jul 20, 2026
37072b8
refator(app): Reduce cyclo for Run, Resolves #13
TheRootDaemon Jul 20, 2026
55554b4
fix(app): Fixed resource leaks
TheRootDaemon Jul 20, 2026
da3a6a5
feat(app): Include contribution links
TheRootDaemon Jul 20, 2026
692e141
cli: Add completions
TheRootDaemon Jul 20, 2026
8a04198
cmd: Error handling, similar flags
TheRootDaemon Jul 25, 2026
fb70c62
tests: Fix flaky tests
TheRootDaemon Jul 26, 2026
7d8d3da
cmd: Change help flag
TheRootDaemon Jul 27, 2026
264cdbc
feat(browse): Add cross-platform browser capabilities for --browse
TheRootDaemon Jul 27, 2026
e56b2d6
feat(cmd): Add -b,--browse flag
TheRootDaemon Jul 27, 2026
4971cbc
feat(cmd): Require a page argument for -b,--browse
TheRootDaemon Jul 27, 2026
5caeeff
feat(render): Add BuildViewURL
TheRootDaemon Jul 27, 2026
b704948
feat(app): Implement -b,--browse
TheRootDaemon Jul 27, 2026
e1e7bcd
tests: Add cases for darwin, windows. Skip irrelevant tests
TheRootDaemon Jul 27, 2026
b1f80f8
chore: Add help string for -b,--browse
TheRootDaemon Jul 28, 2026
6c6bc11
cmd: Fix no-op cases
TheRootDaemon Jul 28, 2026
4d01007
bin: tlgc -> tldr
TheRootDaemon Jul 30, 2026
d526c20
init(lint): Linting rules
TheRootDaemon Aug 1, 2026
f76906f
feat(lint): Filename rules
TheRootDaemon Aug 1, 2026
f382f77
docs(lint): Fix docs
TheRootDaemon Aug 1, 2026
3c000f7
lint: Implement parser
TheRootDaemon Aug 2, 2026
e95d823
lint: Tests for parse
TheRootDaemon Aug 2, 2026
5df0712
lint: Tests for parse_lines
TheRootDaemon Aug 2, 2026
2f4bd54
lint: Tests for parse_sections
TheRootDaemon Aug 2, 2026
3e2e9eb
lint: Tests for title_rules
TheRootDaemon Aug 2, 2026
10f03c5
lint: Title rules
TheRootDaemon Aug 2, 2026
8c20774
lint: File rules
TheRootDaemon Aug 4, 2026
0bae0de
lint: Description rules
TheRootDaemon Aug 6, 2026
a7932cb
lint: example rules
TheRootDaemon Aug 6, 2026
bf2fd44
lint: command rules
TheRootDaemon Aug 7, 2026
0e2b2b7
lint: global Lint
TheRootDaemon Aug 7, 2026
aa1f264
lint: Remove unused field
TheRootDaemon Aug 7, 2026
78d53e0
fix(lint): align whitespace rules with reference behavior
TheRootDaemon Aug 8, 2026
d6a2792
fix(lint): report TLDR105 only for extra commands
TheRootDaemon Aug 8, 2026
59212fc
lint: Add specifications
TheRootDaemon Aug 8, 2026
b5db45e
fix(lint): Avoid platform dependent filename checks
TheRootDaemon Aug 10, 2026
f9e2f02
tests(cache): Add deterministic times to avoid brittle edge cases,
TheRootDaemon Aug 10, 2026
9f8d31e
tests(cache): Refactor similar tests into table driven tests
TheRootDaemon Aug 10, 2026
4234b5e
lint: Format
TheRootDaemon Aug 10, 2026
62f2b00
test(lint): Tests for error strings
TheRootDaemon Aug 10, 2026
ce58d3d
cmd: Add flags/modifiers for --lint, --format
TheRootDaemon Aug 10, 2026
10d62f4
test(cache): Fix brittle test on info_test
TheRootDaemon Aug 10, 2026
fb6b127
chore
TheRootDaemon Aug 12, 2026
1db1280
fix(lint): Reach flag dependency error for --output before parsing
TheRootDaemon Aug 12, 2026
1d69364
feat(app): Lint
TheRootDaemon Aug 12, 2026
9804675
chore(help): Add help strings, completions
TheRootDaemon Aug 12, 2026
a74a9f9
app: Format handlers, linting refactors
TheRootDaemon Aug 13, 2026
e1b8d57
fix(lint): 1-indexed linting
TheRootDaemon Aug 14, 2026
b11e343
chore: Add help strings, completions for --format
TheRootDaemon Aug 14, 2026
fba6015
chore: Improve TUI
TheRootDaemon Aug 14, 2026
f5cb14e
chore: Move error as the last argument
TheRootDaemon Aug 14, 2026
1297764
chore: Format test files
TheRootDaemon Aug 15, 2026
b49b0c9
feat(cmd): Enforce parent-child flag dependencies
TheRootDaemon Aug 16, 2026
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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
* text=auto eol=lf

# Required for TLDR010 (Only Unix-style line endings allowed)
internal/lint/specs/pages/failing/010.md binary
17 changes: 17 additions & 0 deletions browser/browse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package browser

import "os/exec"

// browse starts the named program with the given arguments
// and waits for it to complete.
var browse = func(name string, args ...string) error {
// #nosec G204
// executable names are hardcoded by this package
// and never originate from user input.
cmd := exec.Command(name, args...)
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil

return cmd.Run()
}
51 changes: 51 additions & 0 deletions browser/browse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package browser

import (
"runtime"
"testing"

"github.com/stretchr/testify/assert"
)

func TestRun(t *testing.T) {
tests := []struct {
name string
command string
args []string
wantErr bool
}{
{
name: "valid_command",
command: "echo",
args: []string{"hello"},
wantErr: false,
},
{
name: "invalid_command",
command: "nonexistent-command-xyz",
args: nil,
wantErr: true,
},
{
name: "command_with_empty_args",
command: "echo",
args: []string{},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" && tt.command == "echo" {
t.Skip("echo is not a standalone executable on Windows")
}
err := browse(tt.command, tt.args...)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
50 changes: 50 additions & 0 deletions browser/open.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package browser

import (
"fmt"
"os"
"runtime"
)

// Open opens the given URL in the system browser.
// It detects the platform and chooses the appropriate command.
//
// On macOS and Windows it checks for remote/SSH sessions first.
// On Linux (including WSL) it checks for an active display server.
// Returns an error if no display server is available.
func Open(url string) error {
switch runtime.GOOS {
case "darwin":
return browse("open", url)
case "windows":
return browse("explorer.exe", url)
case "linux":
return openOnLinux(url)
default:
return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
}
}

// openOnLinux handles browser opening on Linux and WSL.
func openOnLinux(url string) error {
if runningOnWSL() {
return openOnWSL(url)
}

if !hasDisplay() {
return fmt.Errorf("no display server detected")
}

return browse("xdg-open", url)
}

// openOnWSL handles browser opening inside Windows Subsystem for Linux.
func openOnWSL(url string) error {
return browse("explorer.exe", url)
}

// hasDisplay checks whether a display server is available
// by testing the DISPLAY and WAYLAND_DISPLAY environment variables.
func hasDisplay() bool {
return os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != ""
}
214 changes: 214 additions & 0 deletions browser/open_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package browser

import (
"runtime"
"testing"

"github.com/stretchr/testify/assert"
)

type call struct {
name string
args []string
}

func TestOpenOnLinux(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("openOnLinux only runs on Linux")
}
if runningOnWSL() {
t.Skip("WSL routes to explorer.exe, tested separately")
}

tests := []struct {
name string
display string
wayland string
wantErr bool
wantCalls []call
}{
{
name: "with display",
display: ":0",
wantCalls: []call{
{name: "xdg-open", args: []string{"https://example.com"}},
},
},
{
name: "no display",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("DISPLAY", tt.display)
t.Setenv("WAYLAND_DISPLAY", tt.wayland)

var calls []call
mockBrowse(t, func(name string, args ...string) error {
calls = append(calls, call{name, args})
return nil
})

err := openOnLinux("https://example.com")
if tt.wantErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), "no display server detected")
assert.Empty(t, calls, "browse should not be called on error")
return
}

assert.NoError(t, err)
assert.Equal(t, tt.wantCalls, calls)
})
}
}

func TestOpenOnWSL(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("openOnWSL only runs on Linux")
}
if !runningOnWSL() {
t.Skip("not running on WSL")
}

var calls []call
mockBrowse(t, func(name string, args ...string) error {
calls = append(calls, call{name, args})
return nil
})

err := openOnWSL("https://example.com")
assert.NoError(t, err)
assert.Equal(
t,
[]call{
{
name: "explorer.exe",
args: []string{"https://example.com"},
},
},
calls,
)
}

func TestOpen(t *testing.T) {
t.Setenv("DISPLAY", "")
t.Setenv("WAYLAND_DISPLAY", "")

switch runtime.GOOS {
case "linux":
if runningOnWSL() {
var calls []call
mockBrowse(t, func(name string, args ...string) error {
calls = append(calls, call{name, args})
return nil
})

err := Open("https://example.com")
assert.NoError(t, err)
assert.Equal(
t,
[]call{
{
name: "explorer.exe",
args: []string{"https://example.com"},
},
}, calls,
)
} else {
err := Open("https://example.com")
assert.Error(t, err)
assert.Contains(t, err.Error(), "no display server detected")
}
case "darwin":
var calls []call
mockBrowse(t, func(name string, args ...string) error {
calls = append(calls, call{name, args})
return nil
})

err := Open("https://example.com")
assert.NoError(t, err)
assert.Equal(
t,
[]call{
{
name: "open",
args: []string{"https://example.com"},
},
},
calls,
)
case "windows":
var calls []call
mockBrowse(t, func(name string, args ...string) error {
calls = append(calls, call{name, args})
return nil
})

err := Open("https://example.com")
assert.NoError(t, err)
assert.Equal(
t,
[]call{
{
name: "explorer.exe",
args: []string{"https://example.com"},
},
},
calls,
)
}
}

func TestHasDisplay(t *testing.T) {
tests := []struct {
name string
display string
wayland string
want bool
}{
{
name: "neither set",
display: "",
wayland: "",
want: false,
},
{
name: "x11 only",
display: ":0",
wayland: "",
want: true,
},
{
name: "wayland only",
display: "",
wayland: "wayland-0",
want: true,
},
{
name: "both set",
display: ":0",
wayland: "wayland-0",
want: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("DISPLAY", tt.display)
t.Setenv("WAYLAND_DISPLAY", tt.wayland)
assert.Equal(t, tt.want, hasDisplay())
})
}
}

// mockBrowse replaces the package-level browse function for the duration of t.
func mockBrowse(t *testing.T, fn func(string, ...string) error) {
t.Helper()
oldBrowse := browse
browse = fn
t.Cleanup(func() { browse = oldBrowse })
}
29 changes: 29 additions & 0 deletions browser/wsl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package browser

import (
"os"
"strings"
)

// runningOnWSL reports whether the current process is running
// inside Windows Subsystem for Linux (WSL).
func runningOnWSL() bool {
return isWSL(os.ReadFile)
}

// isWSL reports whether the system is running inside Windows Subsystem for Linux (WSL)
// by inspecting the Linux kernel version information.
//
// The readFile function is injected to allow the detection logic to be
// tested without reading from the real filesystem.
func isWSL(readFile func(string) ([]byte, error)) bool {
data, err := readFile("/proc/version")
if err != nil {
return false
}

return strings.Contains(
strings.ToLower(string(data)),
"microsoft",
)
}
Loading