Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cookbook - New section on handling keypress loops using input listen #1445

Merged
merged 3 commits into from
Jun 13, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .vuepress/configs/sidebar/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export const sidebarEn: SidebarConfig = {
'/cookbook/files',
'/cookbook/git',
'/cookbook/parsing_git_log',
'/cookbook/input_listen_keys',
'/cookbook/http',
'/cookbook/direnv',
'/cookbook/ssh_agent',
Expand Down
34 changes: 4 additions & 30 deletions book/thinking_in_nu.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,40 +63,14 @@ would work, since the string can be evaluated at compile-time:

For more in-depth explanation, check [How Nushell Code Gets Run](how_nushell_code_gets_run.md).

## Variables are immutable
## Variables are immutable by default

Another common surprise for folks coming from other languages is that Nushell variables are immutable (and indeed some people have started to call them "constants" to reflect this). Coming to Nushell you'll want to spend some time becoming familiar with working in a more functional style, as this tends to help write code that works best with immutable variables.

You might wonder why Nushell uses immutable variables. Early on in Nushell's development we decided to see how long we could go using a more data-focused, functional style in the language. More recently, we added a key bit of functionality into Nushell that made these early experiments show their value: parallelism. By switching from [`each`](/commands/docs/each.md) to [`par-each`](/commands/docs/par-each.md) in any Nushell script, you're able to run the corresponding block of code in parallel over the input. This is possible because Nushell's design leans heavily on immutability, composition, and pipelining.

Just because Nushell variables are immutable doesn't mean things don't change. Nushell makes heavy use of the technique of "shadowing". Shadowing means creating a new variable with the same name as a previously declared variable. For example, say you had an `$x` in scope, and you wanted a new `$x` that was one greater:

```nu
let x = $x + 1
```

This new `x` is visible to any code that follows this line. Careful use of shadowing can make for an easier time working with variables, though it's not required.

Loop counters are another common pattern for mutable variables and are built into most iterating commands, for example you can get both each item and an index of each item using [`each`](/commands/docs/each.md):

```nu
> ls | enumerate | each { |it| $"Number ($it.index) is size ($it.item.size)" }
```

You can also use the [`reduce`](/commands/docs/reduce.md) command to work in the same way you might mutate a variable in a loop. For example, if you wanted to find the largest string in a list of strings, you might do:

```nu
> [one, two, three, four, five, six] | reduce {|curr, max|
if ($curr | str length) > ($max | str length) {
$curr
} else {
$max
}
}
```
Another common surprise for folks coming from other languages is that Nushell variables are immutable by default. Coming to Nushell, you'll want to spend some time becoming familiar with working in a more functional style, as this tends to help write code that works best with immutable variables.

**Thinking in Nushell:** If you're used to using mutable variables for different tasks, it will take some time to learn how to do each task in a more functional style. Nushell has a set of built-in capabilities to help with many of these patterns, and learning them will help you write code in a more Nushell-style. The added benefit of speeding up your scripts by running parts of your code in parallel is a nice bonus.

See [Immutable Variables](variables.html#immutable-variables) and [Choosing between mutable and immutable variables](variables.html#choosing-between-mutable-and-immutable-variables) for more information.

## Nushell's environment is scoped

Nushell takes multiple design cues from compiled languages. One such cue is that languages should avoid global mutable state. Shells have commonly used global mutation to update the environment, but Nushell steers clear of this approach.
Expand Down
127 changes: 125 additions & 2 deletions book/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,30 @@ An immutable variable cannot change its value after declaration. They are declar

```nu
> let val = 42
> print $val
> $val
42
> $val = 100
Error: nu::shell::assignment_requires_mutable_variable

× Assignment to an immutable variable.
╭─[entry #10:1:1]
1 │ $val = 100
· ──┬─
· ╰── needs to be a mutable variable
╰────
```

However, they can be 'shadowed'. Shadowing means that they are redeclared and their initial value cannot be used anymore within the same scope.
However, immutable variables can be 'shadowed'. Shadowing means that they are redeclared and their initial value cannot be used anymore within the same scope.

```nu
> let val = 42 # declare a variable
> do { let val = 101; $val } # in an inner scope, shadow the variable
101
> $val # in the outer scope the variable remains unchanged
42
> let val = $val + 1 # now, in the outer scope, shadow the original variable
> $val # in the outer scope, the variable is now shadowed, and
43 # its original value is no longer available.
```

### Mutable Variables
Expand Down Expand Up @@ -77,6 +89,117 @@ const script_file = 'path/to/script.nu'
source $script_file
```

## Choosing between mutable and immutable variables

Try to use immutable variables for most use-cases.

You might wonder why Nushell uses immutable variables by default. For the first few years of Nushell's development, mutable variables were not a language feature. Early on in Nushell's development, we decided to see how long we could go using a more data-focused, functional style in the language. This experiment showed its value when Nushell introduced parallelism. By switching from [`each`](/commands/docs/each.md) to [`par-each`](/commands/docs/par-each.md) in any Nushell script, you're able to run the corresponding block of code in parallel over the input. This is possible because Nushell's design leans heavily on immutability, composition, and pipelining.

Many, if not most, use-cases for mutable variables in Nushell have a functional solution that:

- Only uses immutable variables, and as a result ...
- Has better performance
- Supports streaming
- Can support additional features such as `par-each` as mentioned above

For instance, loop counters are a common pattern for mutable variables and are built into most iterating commands. For example, you can get both each item and the index of each item using [`each`](/commands/docs/each.md) with [`enumerate`](/commands/docs/enumerate.md):

```nu
> ls | enumerate | each { |it| $"Item #($it.index) is size ($it.item.size)" }
╭───┬───────────────────────────╮
│ 0 │ Item #0 is size 812 B │
│ 1 │ Item #1 is size 3.4 KiB │
│ 2 │ Item #2 is size 11.0 KiB │
│ 3 │ ... │
│ 4 │ Item #18 is size 17.8 KiB │
│ 5 │ Item #19 is size 482 B │
│ 6 │ Item #20 is size 4.0 KiB │
╰───┴───────────────────────────╯
```

You can also use the [`reduce`](/commands/docs/reduce.md) command to work in the same way you might mutate a variable in a loop. For example, if you wanted to find the largest string in a list of strings, you might do:

```nu
> [one, two, three, four, five, six] | reduce {|current_item, max|
if ($current_item | str length) > ($max | str length) {
$current_item
} else {
$max
}
}

three
```

While `reduce` processes lists, the [`generate`](/commands/docs/generate.md) command can be used with arbitrary sources such as external REST APIs, also without requiring mutable variables. Here's an example that retrieves local weather data every hour and generates a continuous list from that data. The `each` command can be used to consume each new list item as it becomes available.

```nu
generate khot {|weather_station|
let res = try {
http get -ef $'https://api.weather.gov/stations/($weather_station)/observations/latest'
} catch {
null
}
sleep 1hr
match $res {
null => {
next: $weather_station
}
_ => {
out: ($res.body? | default '' | from json)
next: $weather_station
}
}
}
| each {|weather_report|
{
time: ($weather_report.properties.timestamp | into datetime)
temp: $weather_report.properties.temperature.value
}
}
```

### Performance considerations

Using [filter commands](/commands/categories/filters.html) with immutable variables is often far more performant than mutable variables with traditional flow-control statements such as `for` and `while`. For example:

- Using a `for` statement to create a list of 50,000 random numbers:

```nu
timeit {
mut randoms = []
for _ in 1..50_000 {
$randoms = ($randoms | append (random int))
}
}
```

Result: 1min 4sec 191ms 135µs 90ns

- Using `each` to do the same:

```nu
timeit {
let randoms = (1..50_000 | each {random int})
}
```

Result: 19ms 314µs 205ns

- Using `each` with 10,000,000 iterations:

```nu
timeit {
let randoms = (1..10_000_000 | each {random int})
}
```

Result: 4sec 233ms 865µs 238ns

As with many filters, the `each` statement also streams its results, meaning the next stage of the pipeline can continue processing without waiting for the results to be collected into a variable.

For tasks which can be optimized by parallelization, as mentioned above, `par-each` can have even more drastic performance gains.

## Variable Names

Variable names in Nushell come with a few restrictions as to what characters they can contain. In particular, they cannot contain these characters:
Expand Down
187 changes: 187 additions & 0 deletions cookbook/input_listen_keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
---
title: Acting on keypresses using `input listen`
---

# Acting on keypresses using `input listen`

A common "key listening" pattern is to:

- Listen for a specific key (or one of a set of keys) to be pressed
- Take action depending on which key was pressed
- Loop if one of the expected keys wasn't pressed

There are several patterns that can accomplish this, each with advantages and disadvantages. You can choose from one of the following patterns that best fits your use-case and coding style:

1. A first attempt might be the following simple loop. This will work for some cases, but a `loop` cannot itself return a _value_:

```nu
def run_some_code [] {
print "I'm running the code, but I can't return a"
print "value because I need to `break` out of the loop."
42
break
}

print '(a) Run some code (x) Exit'

loop {
let key = (input listen --types [key])
if ($key.code == 'a') and ($key.modifiers == []) {
run_some_code
} else if ($key.code == 'x') and ($key.modifiers == []) {
print 'User exited'
break
} else if ($key.code == 'c') and ($key.modifiers == ['keymodifiers(control)']) {
print 'Terminated with Ctrl-C'
break
} else {
print "That key wasn't recognized."
print 'Press (a) to run some code or (x) to Exit'
continue
}
}
```

2. If you need to return a value, you can use a mutable variable to hold the key result after the input loop has ended, _then_ return a value based on the captured keypress:

```nu
def run_some_code [] {
print "I'm running the code and returning 42"
42
}

mut key_props = []
print '(a) Run some code (x) Exit'

loop {
let key = (input listen --types [key])
$key_props = [$key.code $key.modifiers]
let valid_keys = [
[ 'a' [] ]
[ 'x' [] ]
[ 'c' ['keymodifiers(control)'] ]
]

if $key_props in $valid_keys {
break
} else {
print "That key wasn't recognized."
print 'Press (a) to run some code or (x) to Exit'
continue
}
}

# Act on the captured keypress from the mutable variable
if $key_props == [ 'a' [] ] {
run_some_code
} else if $key_props == [ 'x' [] ] {
print 'User exited'
} else if $key_props == [ 'c' ['keymodifiers(control)'] ] {
print 'Terminated with Ctrl-C'
}
```

3. This version uses a custom command that recursively calls itself until one of the desired keys is pressed. However, keep in mind that Nushell limits the number of recursive calls based on the value of `$env.config.recursion_limit` (default 50). Hold down the <kbd>y</kbd> key (not monitored) to demonstrate an early exit based on recursion limits.

Note that `break` statements are not needed in this version.

```nu
def run_some_code [] {
print "I'm running the code and returning 42"
42
}

print '(a) Run some code (x) Exit'

def input_loop [] {
let key = (input listen --types [key])
if ($key.code == 'a') and ($key.modifiers == []) {
run_some_code
} else if ($key.code == 'x') and ($key.modifiers == []) {
print 'User exited'
} else if ($key.code == 'c') and ($key.modifiers == ['keymodifiers(control)']) {
print 'Terminated with Ctrl-C'
} else {
print "That key wasn't recognized."
print 'Press (a) to run some code or (x) to Exit'
# Recurse
input_loop
}
}
# Start the loop
try {
input_loop
} catch {|e| print ($e.debug)}
```

4. The `generate` command offers a functional loop alternative, without recursion limits or mutable variables. `generate` can also collect multiple results into a list, and the output is streamed.

```nu
def run_some_code [] {
print "I'm running the code and returning 42"
42
}

print '(a) Run some code (x) Exit'

let key_generator = {|_|
let key = (input listen --types [key])

if ($key.code == 'a') and ($key.modifiers == []) {
# Returning an "out" record without a "next" terminates the loop
{ out: (run_some_code) }
} else if ($key.code == 'x') and ($key.modifiers == []) {
print 'User exited'
{ out: null }
} else if ($key.code == 'c') and ($key.modifiers == ['keymodifiers(control)']) {
print 'Terminated with Ctrl-C'
{ out: null }
} else {
print "That key wasn't recognized."
print 'Press (a) to run some code or (x) to Exit'
# Next key generation
{ next: null }
}
}

generate null $key_generator | get 0
```

## Using match statements with a list of keycodes

The above examples use `if`/`else` statements with hard-coded key values. You may find it easier to maintain your code using `match` statements with a list of keycodes and modifiers. Using this technique, the second example above might look like:

```nu
def run_some_code [] {
print "I'm running the code and returning 42"
42
}

let keys = {
# [ key.code key.modifiers ]
a: [ 'a' [] ]
x: [ 'x' [] ]
ctrl-c: [ 'c' ['keymodifiers(control)'] ]
}
mut key = {keycode: '', modifiers: ['']}
print '(a) Run some code (x) Exit'

loop {
$key = (input listen --types [key])
match [$key.code $key.modifiers] {
$keymatch if $keymatch == $keys.a => {break}
$keymatch if $keymatch == $keys.x => {print 'User exited'; break}
$keymatch if $keymatch == $keys.ctrl-c => {print 'Terminated with Ctrl-C'; break}
_ => {
print "That key wasn't recognized"
print 'Press (a) to run some code or (x) to Exit'
continue
}
}
}

# Act on the captured keypress from the mutable variable
match [$key.code $key.modifiers] {
$k if $k == $keys.a => {run_some_code}
}
```