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
17 changes: 15 additions & 2 deletions lib/cli.ml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,25 @@ let ignore_size_warning_arg =
let doc = "Ignore the minimum size warning." in
Arg.(value & flag & info [ "i"; "ignore-size-warning" ] ~doc)

let no_gitignore_arg =
let doc = "Show gitignored files in file tree." in
Arg.(value & flag & info [ "no-gitignore" ] ~doc)

let no_nerd_font_arg =
let doc = "Don't try to use Nerd Font Icons." in
Arg.(value & flag & info [ "n"; "no-nerd-font" ] ~doc)

let run owner_repo local_path log_file ignore_size_warning no_nerd_font =
let run owner_repo local_path log_file ignore_size_warning no_gitignore
no_nerd_font =
Tui.start
{ owner_repo; local_path; log_file; ignore_size_warning; no_nerd_font }
{
owner_repo;
local_path;
log_file;
ignore_size_warning;
show_gitignored = no_gitignore;
no_nerd_font;
}

let gh_tui_term =
Term.(
Expand All @@ -37,6 +49,7 @@ let gh_tui_term =
$ path_arg
$ log_arg
$ ignore_size_warning_arg
$ no_gitignore_arg
$ no_nerd_font_arg)

let cmd =
Expand Down
2 changes: 1 addition & 1 deletion lib/fs/dune
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
(name fs)
(libraries
str
;; Internal dependencies
re
pretty))
114 changes: 86 additions & 28 deletions lib/fs/fs.ml
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
module Filec = Filec
module Gitignore = Gitignore

type tree =
| File of {
name : string;
contents : Filec.t Lazy.t;
file_type : Filec.file_type Lazy.t;
ignored : bool;
}
| Dir of {
name : string;
children : tree array Lazy.t;
ignored : bool;
}

type dir_cursor = {
Expand Down Expand Up @@ -38,39 +41,80 @@ let order_files t1 t2 =

let rec sort_tree = function
| File _ as f -> f
| Dir { name; children = (lazy children) } ->
| Dir { name; children = (lazy children); ignored } ->
Array.sort order_files children;
Dir { name; children = lazy (Array.map sort_tree children) }
Dir { name; children = lazy (Array.map sort_tree children); ignored }

let filter_visible ~show_ignored files =
if show_ignored then files
else
files
|> Array.to_list
|> List.filter (function
| File { ignored = true; _ } | Dir { ignored = true; _ } -> false
| _ -> true)
|> Array.of_list

let relative_path ~root path =
let prefix =
if Filename.check_suffix root Filename.dir_sep then root
else root ^ Filename.dir_sep
in
if String.starts_with ~prefix path then
String.sub path (String.length prefix)
(String.length path - String.length prefix)
else path

(* Recursively reads a directory tree *)
let rec to_tree path =
let rec to_tree ~patterns ~root path =
let name = Filename.basename path in
let rel_path =
let rel = relative_path ~root path in
if Sys.is_directory path then rel ^ Filename.dir_sep else rel
in
let ignored = Gitignore.is_ignored rel_path patterns in
if Sys.is_directory path then
let children =
lazy
(Array.map
(fun child_name -> to_tree (Filename.concat path child_name))
(Sys.readdir path))
(Sys.readdir path
|> Array.to_list
|> List.map (Filename.concat path)
|> List.map (to_tree ~patterns ~root)
|> Array.of_list)
in
let name = Filename.basename path in
Dir { name; children }
Dir { name; ignored; children }
else
File
{
name = Filename.basename path;
name;
ignored;
contents = lazy (Filec.read path);
file_type = lazy (Filec.type_of_path path);
}

let read_tree path = path |> to_tree |> sort_tree
let ignore_patterns path =
match Gitignore.find_gitignore path with
| Some content -> Gitignore.parse content
| None -> []

let read_tree path =
to_tree ~patterns:(ignore_patterns path) ~root:path path |> sort_tree

let file_at cursor = cursor.files.(cursor.pos)

type zipper = {
parents : dir_cursor list;
current : cursor;
show_ignored : bool;
}

let zip_it trees =
{ parents = []; current = Dir_cursor { pos = 0; files = trees } }
let zip_it trees ~show_ignored =
let visible = filter_visible ~show_ignored trees in
{
parents = [];
current = Dir_cursor { pos = 0; files = visible };
show_ignored;
}

let zipper_parents zipper =
List.map (fun cursor -> file_name (file_at cursor)) zipper.parents
Expand All @@ -84,8 +128,10 @@ let move_cursor move_dir_cursor move_file_cursor = function

let move_dir_cursor move cursor =
let len = Array.length cursor.files in
let new_pos = (cursor.pos + move + len) mod len in
{ cursor with pos = new_pos }
if len = 0 then cursor
else
let new_pos = (cursor.pos + move + len) mod len in
{ cursor with pos = new_pos }

let move_file_cursor move cursor =
let len = Filec.length cursor in
Expand All @@ -106,23 +152,35 @@ let go_up = go_move (-1)
let go_next zipper =
match zipper.current with
| File_cursor _ -> zipper
| Dir_cursor cursor -> (
let next = file_at cursor in
match next with
| File { contents; _ } ->
{
parents = cursor :: zipper.parents;
current = File_cursor (Lazy.force contents);
}
| Dir { children = (lazy next); _ } ->
if Array.length next = 0 then zipper
else
| Dir_cursor cursor ->
if Array.length cursor.files = 0 then zipper
else
let next = file_at cursor in
match next with
| File { contents; _ } ->
{
parents = cursor :: zipper.parents;
current = Dir_cursor { pos = 0; files = next };
})
current = File_cursor (Lazy.force contents);
show_ignored = zipper.show_ignored;
}
| Dir { children = (lazy children); _ } ->
let visible =
filter_visible ~show_ignored:zipper.show_ignored children
in
if Array.length visible = 0 then zipper
else
{
parents = cursor :: zipper.parents;
current = Dir_cursor { pos = 0; files = visible };
show_ignored = zipper.show_ignored;
}

let go_back zipper =
match zipper.parents with
| [] -> zipper
| current :: parents -> { parents; current = Dir_cursor current }
| current :: parents ->
{
parents;
current = Dir_cursor current;
show_ignored = zipper.show_ignored;
}
6 changes: 5 additions & 1 deletion lib/fs/fs.mli
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(** Module for manipulating different file contents (text, binary) to get lines
and current offsets. *)
module Filec = Filec
module Gitignore = Gitignore

(* NOTE: contents and file_type are stored separately so we can know the file
type and assign a proper icon without reading the contents *)
Expand All @@ -11,10 +12,12 @@ type tree =
name : string;
contents : Filec.t Lazy.t;
file_type : Filec.file_type Lazy.t;
ignored : bool;
}
| Dir of {
name : string;
children : tree array Lazy.t;
ignored : bool;
}

(** Return the name of a given tree node. *)
Expand All @@ -40,10 +43,11 @@ val file_at : dir_cursor -> tree
type zipper = {
parents : dir_cursor list;
current : cursor;
show_ignored : bool;
}

(** Constructs a zipper from the contents of a given directory. *)
val zip_it : tree array -> zipper
val zip_it : tree array -> show_ignored:bool -> zipper

(** Returns the list of parents names in reverse order. *)
val zipper_parents : zipper -> string list
Expand Down
62 changes: 62 additions & 0 deletions lib/fs/gitignore.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module Gitignore = Gitignore

type rule = {
pattern : string;
negated : bool;
}

let strip_prefix ~prefix text =
if String.starts_with ~prefix text then
String.sub text (String.length prefix)
(String.length text - String.length prefix)
else text

let normalize_path path =
path
|> strip_prefix ~prefix:"./"
|> strip_prefix ~prefix:Filename.dir_sep

let find_gitignore path =
let full_path = Filename.concat path ".gitignore" in
if Sys.file_exists full_path then (
let ic = open_in full_path in
let content = really_input_string ic (in_channel_length ic) in
close_in ic;
Some content)
else None

let parse (content : string) : rule list =
content
|> String.split_on_char '\n'
|> List.map String.trim
|> List.filter (fun line ->
line <> "" && not (String.starts_with ~prefix:"#" line))
|> List.map (fun line ->
if String.starts_with ~prefix:"!" line then
{
pattern = String.sub line 1 (String.length line - 1);
negated = true;
}
else { pattern = line; negated = false })

let matches path rule =
let normalized_path = normalize_path path in
let normalized_pattern = normalize_path rule.pattern in
if normalized_pattern = "" then false
else if String.ends_with ~suffix:Filename.dir_sep normalized_pattern then
String.starts_with ~prefix:normalized_pattern normalized_path
else
try
let re = Re.Glob.glob ~anchored:true normalized_pattern |> Re.compile in
Re.execp re normalized_path
with _ -> false

let is_ignored path rules =
let rec apply rules ignored =
match rules with
| [] -> ignored
| rule :: rest ->
if matches path rule then apply rest (not rule.negated)
else apply rest ignored
in
apply rules false
19 changes: 19 additions & 0 deletions lib/fs/gitignore.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
(** Module to manage gitignored files visability **)
module Gitignore = Gitignore

(** A parsed .gitignore rule *)
type rule = {
pattern : string;
negated : bool;
}

(** Parse .gitignore content into a list of rules. Supports ! negation *)
val parse : string -> rule list

(** Check if a path is ignored according to the parsed rules *)
val is_ignored : string -> rule list -> bool

(** Match a path against a single rule *)
val matches : string -> rule -> bool

val find_gitignore : string -> string option
3 changes: 3 additions & 0 deletions lib/pretty/style.ml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ let directory = Styling.(bold & fg Color.magenta)
(* When a file in a tree viewer is chosen. *)
let chosen = Styling.(bold & fg Color.magenta)

(* Mark Gitignored files *)
let gitignored = Styling.(bold & fg Color.bright_black)

(* Additional helper text. *)
let secondary = Styling.(fg Color.cyan)
let bold = Styling.bold
Expand Down
23 changes: 20 additions & 3 deletions lib/tui/init/init.ml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ let get_terminal_dimensions ignore_size_warning =
{|⚠️ Terminal size is too small! GitHub TUI works better on bigger terminals.
Expected size: %3d width x %3d height
But got: %3d width x %3d height

Pass the --ignore-size-warning flag to run anyway.
|}
min_width min_height width height;
Expand All @@ -71,14 +71,31 @@ type t = {
local_path : string option;
log_file : string option;
ignore_size_warning : bool;
show_gitignored : bool;
no_nerd_font : bool;
}

let init
{ owner_repo; local_path; ignore_size_warning; log_file = _; no_nerd_font }
{
owner_repo;
local_path;
ignore_size_warning;
log_file = _;
show_gitignored;
no_nerd_font;
}
: Model.initial_data =
let ({ owner; repo } as owner_repo) = parse_owner_repo owner_repo in
let root_dir_path = clone_repo ~owner_repo ~local_path in
let files = Lazy.force (read_root_tree ~root_dir_path) in
let { height; width } = get_terminal_dimensions ignore_size_warning in
{ owner; repo; root_dir_path; files; width; height; no_nerd_font }
{
owner;
repo;
root_dir_path;
show_gitignored;
files;
width;
height;
no_nerd_font;
}
1 change: 1 addition & 0 deletions lib/tui/init/init.mli
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ type t = {
local_path : string option;
log_file : string option;
ignore_size_warning : bool;
show_gitignored : bool;
no_nerd_font : bool;
}

Expand Down
Loading