diff --git a/examples/interactive/cli-platform/Dir.roc b/examples/interactive/cli-platform/Dir.roc new file mode 100644 index 0000000000..cf5a615815 --- /dev/null +++ b/examples/interactive/cli-platform/Dir.roc @@ -0,0 +1,25 @@ +interface Dir + exposes [ReadErr, DeleteErr, DirEntry, deleteEmptyDir, deleteRecursive, list] + imports [Effect, Task.{ Task }, InternalTask, Path.{ Path }, InternalPath, InternalDir] + +ReadErr : InternalDir.ReadErr + +DeleteErr : InternalDir.DeleteErr + +DirEntry : InternalDir.DirEntry + +## Lists the files and directories inside the directory. +list : Path -> Task (List Path) [DirReadErr Path ReadErr]* [Read [File]*]* +list = \path -> + effect = Effect.map (Effect.dirList (InternalPath.toBytes path)) \result -> + when result is + Ok entries -> Ok (List.map entries InternalPath.fromOsBytes) + Err err -> Err (DirReadErr path err) + + InternalTask.fromEffect effect + +## Deletes a directory if it's empty. +deleteEmptyDir : Path -> Task {} [DirDeleteErr Path DeleteErr]* [Write [File]*]* + +## Recursively deletes the directory as well as all files and directories inside it. +deleteRecursive : Path -> Task {} [DirDeleteErr Path DeleteErr]* [Write [File]*]* \ No newline at end of file diff --git a/examples/interactive/cli-platform/Effect.roc b/examples/interactive/cli-platform/Effect.roc index 92a6fc49ae..26505ddc07 100644 --- a/examples/interactive/cli-platform/Effect.roc +++ b/examples/interactive/cli-platform/Effect.roc @@ -6,6 +6,8 @@ hosted Effect always, forever, loop, + dirList, + cwd, stdoutLine, stderrLine, stdinLine, @@ -15,7 +17,7 @@ hosted Effect fileWriteUtf8, fileWriteBytes, ] - imports [InternalHttp.{ Request, Response }, InternalFile] + imports [InternalHttp.{ Request, Response }, InternalFile, InternalDir] generates Effect with [after, map, always, forever, loop] stdoutLine : Str -> Effect {} @@ -26,5 +28,8 @@ fileWriteBytes : List U8, List U8 -> Effect (Result {} InternalFile.WriteErr) fileWriteUtf8 : List U8, Str -> Effect (Result {} InternalFile.WriteErr) fileDelete : List U8 -> Effect (Result {} InternalFile.WriteErr) fileReadBytes : List U8 -> Effect (Result (List U8) InternalFile.ReadErr) +dirList : List U8 -> Effect (Result (List (List U8)) InternalDir.ReadErr) + +cwd : Effect (List U8) sendRequest : Box Request -> Effect Response diff --git a/examples/interactive/cli-platform/Env.roc b/examples/interactive/cli-platform/Env.roc new file mode 100644 index 0000000000..72a10e8d61 --- /dev/null +++ b/examples/interactive/cli-platform/Env.roc @@ -0,0 +1,15 @@ +interface Env + exposes [cwd] + imports [Task.{ Task }, Path.{ Path }, InternalPath, Effect, InternalTask] + +## Reads the [current working directory](https://en.wikipedia.org/wiki/Working_directory) +## from the environment. +cwd : Task Path [CwdUnavailable]* [Env]* +cwd = + effect = Effect.map Effect.cwd \bytes -> + if List.isEmpty bytes then + Err CwdUnavailable + else + Ok (InternalPath.fromArbitraryBytes bytes) + + InternalTask.fromEffect effect diff --git a/examples/interactive/cli-platform/File.roc b/examples/interactive/cli-platform/File.roc index 257218b082..4fd0b03e61 100644 --- a/examples/interactive/cli-platform/File.roc +++ b/examples/interactive/cli-platform/File.roc @@ -6,6 +6,8 @@ ReadErr : InternalFile.ReadErr WriteErr : InternalFile.WriteErr +## Encodes a value using the given `EncodingFormat` and writes it to a file. +## ## For example, suppose you have a [JSON](https://en.wikipedia.org/wiki/JSON) ## `EncodingFormat` named `Json.toCompactUtf8`. Then you can use that format ## to write some encodable data to a file as JSON, like so: @@ -31,7 +33,7 @@ write = \path, val, fmt -> # TODO handle encoding errors here, once they exist writeBytes path bytes -## Write bytes to a file. +## Writes bytes to a file. ## ## # Writes the bytes 1, 2, 3 to the file `myfile.dat`. ## File.writeBytes (Path.fromStr "myfile.dat") [1, 2, 3] @@ -43,7 +45,7 @@ writeBytes : Path, List U8 -> Task {} [FileWriteErr Path WriteErr]* [Write [File writeBytes = \path, bytes -> toWriteTask path \pathBytes -> Effect.fileWriteBytes pathBytes bytes -## Write a [Str] to a file, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8). +## Writes a [Str] to a file, encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8). ## ## # Writes "Hello!" encoded as UTF-8 to the file `myfile.txt`. ## File.writeUtf8 (Path.fromStr "myfile.txt") "Hello!" @@ -55,7 +57,7 @@ writeUtf8 : Path, Str -> Task {} [FileWriteErr Path WriteErr]* [Write [File]*]* writeUtf8 = \path, str -> toWriteTask path \bytes -> Effect.fileWriteUtf8 bytes str -## Delete a file from the filesystem. +## Deletes a file from the filesystem. ## ## # Deletes the file named ## File.delete (Path.fromStr "myfile.dat") [1, 2, 3] @@ -75,7 +77,7 @@ delete : Path -> Task {} [FileWriteErr Path WriteErr]* [Write [File]*]* delete = \path -> toWriteTask path \bytes -> Effect.fileDelete bytes -## Read all the bytes in a file. +## Reads all the bytes in a file. ## ## # Read all the bytes in `myfile.txt`. ## File.readBytes (Path.fromStr "myfile.txt") @@ -87,7 +89,7 @@ readBytes : Path -> Task (List U8) [FileReadErr Path ReadErr]* [Read [File]*]* readBytes = \path -> toReadTask path \bytes -> Effect.fileReadBytes bytes -## Read a [Str] from a file containing [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-encoded text. +## Reads a [Str] from a file containing [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-encoded text. ## ## # Reads UTF-8 encoded text into a `Str` from the file `myfile.txt`. ## File.readUtf8 (Path.fromStr "myfile.txt") diff --git a/examples/interactive/cli-platform/FileMetadata.roc b/examples/interactive/cli-platform/FileMetadata.roc new file mode 100644 index 0000000000..89386f259f --- /dev/null +++ b/examples/interactive/cli-platform/FileMetadata.roc @@ -0,0 +1,35 @@ +interface FileMetadata + exposes [FileMetadata, bytes, type, isReadonly, mode] + imports [] + +# Design note: this is an opaque type rather than a type alias so that +# we can add new operating system info if new OS releases introduce them, +# as a backwards-compatible change. + +FileMetadata := { + bytes : U64, + type : [File, Dir, Symlink], + isReadonly : Bool, + mode : [Unix U32, NonUnix], +} + +bytes : FileMetadata -> U64 +bytes = \@FileMetadata info -> info.bytes + +isReadonly : FileMetadata -> Bool +isReadonly = \@FileMetadata info -> info.isReadonly + +type : FileMetadata -> [File, Dir, Symlink] +type = \@FileMetadata info -> info.type + +mode : FileMetadata -> [Unix U32, NonUnix] +mode = \@FileMetadata info -> info.mode + +# TODO need to create a Time module and return something like Time.Utc here. +# lastModified : FileMetadata -> Utc + +# TODO need to create a Time module and return something like Time.Utc here. +# lastAccessed : FileMetadata -> Utc + +# TODO need to create a Time module and return something like Time.Utc here. +# created : FileMetadata -> Utc \ No newline at end of file diff --git a/examples/interactive/cli-platform/InternalDir.roc b/examples/interactive/cli-platform/InternalDir.roc new file mode 100644 index 0000000000..6163125e84 --- /dev/null +++ b/examples/interactive/cli-platform/InternalDir.roc @@ -0,0 +1,41 @@ +interface InternalDir + exposes [ReadErr, DeleteErr, DirEntry] + imports [FileMetadata.{ FileMetadata }, Path.{ Path }] + +DirEntry : { + path : Path, + type : [File, Dir, Symlink], + metadata : FileMetadata, +} + +ReadErr : [ + NotFound, + Interrupted, + InvalidFilename, + PermissionDenied, + TooManySymlinks, # aka FilesystemLoop + TooManyHardlinks, + TimedOut, + StaleNetworkFileHandle, + NotADirectory, + OutOfMemory, + Unsupported, + Unrecognized I32 Str, +] + +DeleteErr : [ + NotFound, + Interrupted, + InvalidFilename, + PermissionDenied, + TooManySymlinks, # aka FilesystemLoop + TooManyHardlinks, + TimedOut, + StaleNetworkFileHandle, + NotADirectory, + ReadOnlyFilesystem, + DirectoryNotEmpty, + OutOfMemory, + Unsupported, + Unrecognized I32 Str, +] diff --git a/examples/interactive/cli-platform/InternalPath.roc b/examples/interactive/cli-platform/InternalPath.roc index b27ff8e5e5..6731b1a92a 100644 --- a/examples/interactive/cli-platform/InternalPath.roc +++ b/examples/interactive/cli-platform/InternalPath.roc @@ -5,6 +5,8 @@ interface InternalPath wrap, unwrap, toBytes, + fromArbitraryBytes, + fromOsBytes, ] imports [] @@ -61,3 +63,11 @@ toBytes = \@InternalPath path -> FromOperatingSystem bytes -> bytes ArbitraryBytes bytes -> bytes FromStr str -> Str.toUtf8 str + +fromArbitraryBytes : List U8 -> InternalPath +fromArbitraryBytes = \bytes -> + @InternalPath (ArbitraryBytes bytes) + +fromOsBytes : List U8 -> InternalPath +fromOsBytes = \bytes -> + @InternalPath (FromOperatingSystem bytes) \ No newline at end of file diff --git a/examples/interactive/cli-platform/Path.roc b/examples/interactive/cli-platform/Path.roc index 12de18ee32..dad8f989f6 100644 --- a/examples/interactive/cli-platform/Path.roc +++ b/examples/interactive/cli-platform/Path.roc @@ -6,6 +6,7 @@ interface Path WindowsRoot, # toComponents, # walkComponents, + display, fromStr, fromBytes, withExtension, @@ -85,10 +86,10 @@ fromBytes = \bytes -> ## with the given [Charset]. (Use [Env.charset] to get the current system charset.) ## ## For a conversion to [Str] that is lossy but does not return a [Result], see -## [displayUtf8]. +## [display]. # toInner : Path -> [Str Str, Bytes (List U8)] ## Assumes a path is encoded as [UTF-8](https://en.wikipedia.org/wiki/UTF-8), -## and converts it to a string using [Str.displayUtf8]. +## and converts it to a string using [Str.display]. ## ## This conversion is lossy because the path may contain invalid UTF-8 bytes. If that happens, ## any invalid bytes will be replaced with the [Unicode replacement character](https://unicode.org/glossary/#replacement_character) @@ -103,17 +104,21 @@ fromBytes = \bytes -> ## Converting paths to strings can be an unreliable operation, because operating systems ## don't record the paths' encodings. This means it's possible for the path to have been ## encoded with a different character set than UTF-8 even if UTF-8 is the system default, -## which means when [displayUtf8] converts them to a string, the string may include gibberish. +## which means when [display] converts them to a string, the string may include gibberish. ## [Here is an example.](https://unix.stackexchange.com/questions/667652/can-a-file-path-be-invalid-utf-8/667863#667863) ## ## If you happen to know the [Charset] that was used to encode the path, you can use -## [toStrUsingCharset] instead of [displayUtf8]. -# displayUtf8 : Path -> Str -# displayUtf8 = \path -> -# when InternalPath.unwrap path is -# FromStr str -> str -# FromOperatingSystem bytes | ArbitraryBytes bytes -> -# Str.displayUtf8 bytes +## [toStrUsingCharset] instead of [display]. +display : Path -> Str +display = \path -> + when InternalPath.unwrap path is + FromStr str -> str + FromOperatingSystem bytes | ArbitraryBytes bytes -> + when Str.fromUtf8 bytes is + Ok str -> str + # TODO: this should use the builtin Str.display to display invalid UTF-8 chars in just the right spots, but that does not exist yet! + Err _ -> "�" + # isEq : Path, Path -> Bool # isEq = \p1, p2 -> # when InternalPath.unwrap p1 is diff --git a/examples/interactive/cli-platform/src/lib.rs b/examples/interactive/cli-platform/src/lib.rs index 58b7a89cfc..d801fcd8da 100644 --- a/examples/interactive/cli-platform/src/lib.rs +++ b/examples/interactive/cli-platform/src/lib.rs @@ -238,6 +238,50 @@ pub extern "C" fn roc_fx_fileDelete(roc_path: &RocList) -> RocResult<(), Rea } } +#[no_mangle] +pub extern "C" fn roc_fx_cwd() -> RocList { + // TODO instead, call getcwd on UNIX and GetCurrentDirectory on Windows + match std::env::current_dir() { + Ok(path_buf) => os_str_to_roc_path(path_buf.into_os_string().as_os_str()), + Err(_) => { + // Default to empty path + RocList::empty() + } + } +} + +#[no_mangle] +pub extern "C" fn roc_fx_dirList( + // TODO: this RocResult should use Dir.WriteErr - but right now it's File.WriteErr + // because glue doesn't have Dir.WriteErr yet. + roc_path: &RocList, +) -> RocResult>, WriteErr> { + println!("Dir.list..."); + match std::fs::read_dir(path_from_roc_path(roc_path)) { + Ok(dir_entries) => RocResult::ok( + dir_entries + .map(|opt_dir_entry| match opt_dir_entry { + Ok(entry) => os_str_to_roc_path(entry.path().into_os_string().as_os_str()), + Err(_) => { + todo!("handle dir_entry path didn't resolve") + } + }) + .collect::>>(), + ), + Err(_) => { + todo!("handle Dir.list error"); + } + } +} + +#[cfg(target_family = "unix")] +/// TODO convert from EncodeWide to RocPath on Windows +fn os_str_to_roc_path(os_str: &OsStr) -> RocList { + use std::os::unix::ffi::OsStrExt; + + RocList::from(os_str.as_bytes()) +} + #[no_mangle] pub extern "C" fn roc_fx_sendRequest(roc_request: &glue::Request) -> glue::Response { let mut builder = reqwest::blocking::ClientBuilder::new(); diff --git a/examples/interactive/file.roc b/examples/interactive/file.roc index e6f9b3a3fd..d491ec9477 100644 --- a/examples/interactive/file.roc +++ b/examples/interactive/file.roc @@ -1,12 +1,18 @@ app "file-io" packages { pf: "cli-platform/main.roc" } - imports [pf.Stdout, pf.Stderr, pf.Task, pf.File, pf.Path] + imports [pf.Stdout, pf.Stderr, pf.Task, pf.File, pf.Path, pf.Env, pf.Dir] provides [main] to pf -main : Task.Task {} [] [Write [File, Stdout, Stderr], Read [File]] +main : Task.Task {} [] [Write [File, Stdout, Stderr], Read [File], Env] main = path = Path.fromStr "out.txt" task = + cwd <- Env.cwd |> Task.await + cwdStr = Path.display cwd + _ <- Stdout.line "cwd: \(cwdStr)" |> Task.await + dirEntries <- Dir.list cwd |> Task.await + contentsStr = Str.joinWith (List.map dirEntries Path.display) "\n " + _ <- Stdout.line "Directory contents:\n \(contentsStr)\n" |> Task.await _ <- Stdout.line "Writing a string to out.txt" |> Task.await _ <- File.writeUtf8 path "a string!" |> Task.await contents <- File.readUtf8 path |> Task.await @@ -18,4 +24,5 @@ main = Err (FileWriteErr _ Unsupported) -> Stderr.line "Err: Unsupported" Err (FileWriteErr _ (Unrecognized _ other)) -> Stderr.line "Err: \(other)" Err (FileReadErr _ _) -> Stderr.line "Error reading file" - _ -> Stdout.line "Successfully wrote a string to out.txt" + Err _ -> Stderr.line "Uh oh, there was an error!" + Ok _ -> Stdout.line "Successfully wrote a string to out.txt"