in

Tauri: Fast cross-platform desktop app

1671001408tauri app builder

[ad_1]

This tutorial explores Tauri, a modern cross-platform framework for building desktop apps.

content:

  1. What is Tauri?
  2. Tauri vs Electron: A Quick Comparison
  3. Building a note-taking app
  4. Conclusion

For years, Electron has been the de facto cross-platform framework for building desktop apps. Visual Studio Code, MongoDB Compass, and Postman are all great examples of apps built with this framework. Electron is arguably better, but it has some serious drawbacks that some other modern frameworks overcome. Tauri is one of the best of them.

What is Tauri?

Tauri is a modern framework that lets you design, develop, and build cross-platform apps using familiar web technologies like HTML, CSS, and JavaScript on the front end, while leveraging the powerful Rust programming language on the back end.

Tauri is framework agnostic. This means it can be used with any frontend library, such as Vue, React, Svelte, etc. Also, using Rust in Tauri-based projects is completely optional. You can build your entire app using only the JavaScript API provided by Tauri. This not only makes it easier to create new apps, but it also makes it easier to convert an already built web app codebase into a native desktop app. Most of the original code doesn’t need to be changed.

Let’s see why we use Tauri instead of Electron.

Tauri vs Electron: A Quick Comparison

There are three key ingredients to building a truly great app. Apps should be small, fast, and secure. Tauri outperforms Electron on all three.

  • Tauri produces a much smaller binary. As you can see from Tauri’s published benchmark results, a super simple Hello World! Building your app with Electron can be huge (over 120 MB). In contrast, the same Tauri app has a much smaller binary size of less than 2 MB. This is very impressive in my opinion.
  • Tauri app performance is much faster. From the same page above, we can also see that the memory usage of the Tauri app can be nearly half of the equivalent Electron app.
  • Tauri app is very secure. You can read about all the built-in security features that Tauri offers by default on Tauri’s website. However, one notable feature I would like to mention here is the ability for developers to explicitly enable or disable specific APIs. This not only keeps your app safe, but also reduces your binary size.

Building a note-taking app

In this section, we will create a simple note-taking app with the following features:

  • Add and remove notes
  • Rename note title
  • Edit note content with Markdown
  • Preview note content in HTML
  • Save notes to local storage
  • Import and export notes to system hard drive

All project files can be found on GitHub.

getting started

To get started with Tauri, you first need to install Rust and its system dependencies. These are not described here as they will vary depending on the user’s operating system. Follow the instructions for your OS in the documentation.

When you’re ready, run the following command in your chosen directory:

This will guide you through the installation process as shown below.

$ npm create tauri-app

We hope to help you create something special with Tauri!
You will have a choice of one of the UI frameworks supported by the greater web tech community.
This tool should get you quickly started. See our docs at https://tauri.app/

If you haven't already, please take a moment to setup your system.
You may find the requirements here: https://tauri.app/v1/guides/getting-started/prerequisites  
    
Press any key to continue...
? What is your app name? my-notes
? What should the window title be? My Notes
? What UI recipe would you like to add? create-vite (vanilla, vue, react, svelte, preact, lit) (https://vitejs.dev/guide/
? Add "@tauri-apps/api" npm package? Yes
? Which vite template would you like to use? react-ts
>> Running initial command(s)
Need to install the following packages:
  create-vite@3.2.1
Ok to proceed? (y) y

>> Installing any additional needed dependencies

added 87 packages, and audited 88 packages in 19s

9 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

added 2 packages, and audited 90 packages in 7s

10 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
>> Updating "package.json"
>> Running "tauri init"

> my-notes@0.0.0 tauri
> tauri init --app-name my-notes --window-title My Notes --dist-dir ../dist --dev-path http://localhost:5173

✔ What is your frontend dev command? · npm run dev
✔ What is your frontend build command? · npm run build
>> Updating "tauri.conf.json"
>> Running final command(s)

    Your installation completed.

    $ cd my-notes
    $ npm run tauri dev

Please make sure your choices match what I have made. This is mainly scaffolding a React app with Vite and TypeScript support and installing the Tauri API package.

Don’t run the app yet. First, we need to install some additional packages required for our project. Run the following command in terminal:

npm install @mantine/core @mantine/hooks @tabler/icons @emotion/react marked-react

This will install the following packages:

Now we’re ready to test the app, but first let’s see how the project is structured.

my-notes/
├─ node_modules/
├─ public/
├─ src/
│   ├─ assets/
│   │   └─ react.svg
│   ├─ App.css
│   ├─ App.tsx
│   ├─ index.css
│   ├─ main.tsx
│   └─ vite-env.d.ts
├─ src-tauri/
│   ├─ icons/
│   ├─ src/
│   ├─ .gitignore
│   ├─ build.rs
│   ├─ Cargo.toml
│   └─ tauri.config.json
├─ .gitignore
├─ index.html
├─ package-lock.json
├─ package.json
├─ tsconfig.json
├─ tsconfig.node.json
└─ vite.config.ts

The most important thing here is that the React part of the app is src Directories and Rust and other Tauri-specific files are stored in: src-tauriThe only files that need to be modified in the .Tauri directory are: tauri.conf.json, where you can configure your app. Open this file and allowlist key. Replace its contents with:

"allowlist": {
  "dialog": {
    "save": true,
    "open": true,
    "ask": true
  },
  "fs": {
    "writeFile": true,
    "readFile": true,
    "scope": ["$DOCUMENT/*", "$DESKTOP/*"]
  },
  "path": {
    "all": true
  },
  "notification": {
    "all": true
  }
},

Here, for security reasons, as mentioned above, we will only enable the APIs that our app uses. It also restricts access to the file system, with only two exceptions. Documents When Desktop directory. This allows users to export notes to these directories only.

Before we close the file, we need to make one more change.find bundle key. under that key identifier key.and change its value to com.mynotes.devThis is required for app builds because the . identifier must be unique.

Finally, I would like to say that at the end windows The key allows you to set up all window related settings.

"windows": [
  {
    "fullscreen": false,
    "height": 600,
    "resizable": true,
    "title": "My Notes",
    "width": 800
  }
]

As you can see, title The key was set up according to the values ​​you provided during installation.

Now let’s start the app.in the my-notes In the directory run the following command:

You’ll need to wait a while for Tauri to finish setting up and compile all the files for the first time. do not worry. On subsequent builds the process will be much faster. When Tauri is ready, the app window will automatically open. The image below shows what you will see.

New Tauri window

Note: After the app is run or built in development mode, a new target The directory is created inside src-tauriContains all compiled files.In development mode they are debug subdirectories, and in build mode they are placed in release subdirectory.

Now adjust the file to your needs.First, remove index.css When App.css File. next, main.tsx Open the file and replace its contents with:

import React from 'react'
import ReactDOM from 'react-dom/client'
import { MantineProvider } from '@mantine/core'
import App from './App'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <MantineProvider withGlobalStyles withNormalizeCSS>
      <App />
    </MantineProvider>
  </React.StrictMode>
)

This will set up the components of Mantine for you to use.

next, App.tsx Open the file and replace its contents with:

import { useState } from 'react'
import { Button } from '@mantine/core'

function App() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <Button onClick={() => setCount((count) => count + 1)}>count is {count}</Button>
    </div>
  )
}

export default App

Now if you look at the app window, you’ll see something like this:

New blank app window

Click the button to make sure the app is running properly. You may need to debug if something goes wrong. (See note below.)

Note: If your app is running in development mode, right-click the app window and[DevTools]can be opened. inspect from the menu.

Creating basic app functionality

Now let’s create the app skeleton.replaces the contents of App.tsx The following files:

import { useState } from 'react'
import Markdown from 'marked-react'

import { ThemeIcon, Button, CloseButton, Switch, NavLink, Flex, Grid, Divider, Paper, Text, TextInput, Textarea } from '@mantine/core'
import { useLocalStorage } from '@mantine/hooks'
import { IconNotebook, IconFilePlus, IconFileArrowLeft, IconFileArrowRight } from '@tabler/icons'

import { save, open, ask } from '@tauri-apps/api/dialog'
import { writeTextFile, readTextFile } from '@tauri-apps/api/fs'
import { sendNotification } from '@tauri-apps/api/notification'

function App() {
  const [notes, setNotes] = useLocalStorage({ key: "my-notes", defaultValue: [  {
    "title": "New note",
    "content": ""
  }] })

  const [active, setActive] = useState(0)
  const [title, setTitle] = useState("")
  const [content, setContent] = useState("")
  const [checked, setChecked] = useState(false)

  const handleSelection = (title: string, content: string, index: number) => {
    setTitle(title)
    setContent(content)
    setActive(index)
  }

  const addNote = () => {
    notes.splice(0, 0, {title: "New note", content: ""})
    handleSelection("New note", "", 0)
    setNotes([...notes])
  }

  const deleteNote = async (index: number) => {
    let deleteNote = await ask("Are you sure you want to delete this note?", {
      title: "My Notes",
      type: "warning",
    })
    if (deleteNote) {
      notes.splice(index,1)
      if (active >= index) {
        setActive(active >= 1 ? active - 1 : 0)
      }
      if (notes.length >= 1) {
        setContent(notes[index-1].content)
      } else {
        setTitle("")
        setContent("")
      } 
      setNotes([...notes]) 
    }
  }

  return (
    <div>
      <Grid grow m={10}>
        <Grid.Col span="auto">
          <Flex gap="xl" justify="flex-start" align="center" wrap="wrap">
            <Flex>
              <ThemeIcon size="lg" variant="gradient" gradient={{ from: "teal", to: "lime", deg: 90 }}>
                <IconNotebook size={32} />
              </ThemeIcon>
              <Text color="green" fz="xl" fw={500} ml={5}>My Notes</Text>
            </Flex>
            <Button onClick={addNote} leftIcon={<IconFilePlus />}>Add note</Button>
            <Button.Group>
              <Button variant="light" leftIcon={<IconFileArrowLeft />}>Import</Button>
              <Button variant="light" leftIcon={<IconFileArrowRight />}>Export</Button>
            </Button.Group>
          </Flex>

          <Divider my="sm" />

          {notes.map((note, index) => (
            <Flex key={index}>
              <NavLink onClick={() => handleSelection(note.title, note.content, index)} active={index === active} label={note.title} />
              <CloseButton onClick={() => deleteNote(index)} title="Delete note" size="xl" iconSize={20} />
            </Flex>
          ))} 
        </Grid.Col>
        <Grid.Col span={2}>
          <Switch label="Toggle Editor / Markdown Preview"  checked={checked} onChange={(event) => setChecked(event.currentTarget.checked)}/>

          <Divider my="sm" />

          {checked === false && (
            <div>
              <TextInput mb={5} />
              <Textarea minRows={10} />
            </div>
          )}
          {checked && (
            <Paper shadow="lg" p={10}>
              <Text fz="xl" fw={500} tt="capitalize">{title}</Text>

              <Divider my="sm" />

              <Markdown>{content}</Markdown>
            </Paper>
          )}
        </Grid.Col>
      </Grid>
    </div>
  )
}

export default App

There’s a lot of code here, so let’s break it down.

Import required packages

First, import all the required packages like this:

  • markdown parser
  • mantine components
  • mantine hook
  • icon
  • Tauri API
import { useState } from 'react'
import Markdown from 'marked-react'

import { ThemeIcon, Button, CloseButton, Switch, NavLink, Flex, Grid, Divider, Paper, Text, TextInput, Textarea } from '@mantine/core'
import { useLocalStorage } from '@mantine/hooks'
import { IconNotebook, IconFilePlus, IconFileArrowLeft, IconFileArrowRight } from '@tabler/icons'

import { save, open, ask } from '@tauri-apps/api/dialog'
import { writeTextFile, readTextFile } from '@tauri-apps/api/fs'
import { sendNotification } from '@tauri-apps/api/notification'

Setting App Storage and Variables

In the next part useLocalStorage Hook for setting storage for notes.

It also sets some variables for the current note’s title and content, and two more variables to determine which note is selected (active) and whether Markdown preview is enabled (checked).

Finally, create a utility function to handle the memo selection. When a note is selected, the properties of the current note are updated accordingly.

const [notes, setNotes] = useLocalStorage({ key: "my-notes", defaultValue: [  {
  "title": "New note",
  "content": ""
}] })

const [active, setActive] = useState(0)
const [title, setTitle] = useState("")
const [content, setContent] = useState("")
const [checked, setChecked] = useState(false)

const handleSelection = (title: string, content: string, index: number) => {
  setTitle(title)
  setContent(content)
  setActive(index)
}

Addition of add/delete memo function

The next two functions are for adding/removing notes.

addNote() Insert a new note object in the . notes arrangement.it uses handleSelection() Automatically select new notes after adding. I will update my notes at the end. The reason we use the spread operator here is that otherwise the state won’t update. This way I force the state to update and re-render the component so that the note is displayed properly.

const addNote = () => {
  notes.splice(0, 0, {title: "New note", content: ""})
  handleSelection("New note", "", 0)
  setNotes([...notes])
}

const deleteNote = async (index: number) => {
  let deleteNote = await ask("Are you sure you want to delete this note?", {
    title: "My Notes",
    type: "warning",
  })
  if (deleteNote) {
    notes.splice(index,1)
    if (active >= index) {
      setActive(active >= 1 ? active - 1 : 0)
    }
    if (notes.length >= 1) {
      setContent(notes[index-1].content)
    } else {
      setTitle("")
      setContent("")
    } 
    setNotes([...notes]) 
  }
}

deleteNote() Use the ask In the dialog, make sure the user is trying to delete the note and didn’t accidentally click the delete button. If the user confirms the deletion (deleteNote = true) then if Statement is executed:

  • The note is deleted from notes arrangement
  • of active the variable is updated
  • The title and content of the current note are updated
  • of notes array updated

Creating JSX Templates

The template section has two columns.

In the first column, create buttons for adding, importing, and exporting your app’s logo and name, as well as your notes. again, notes An array to render notes.used here handleSelection() To properly update the current note’s properties when the note’s title link is clicked:

<Grid.Col span="auto">
  <Flex gap="xl" justify="flex-start" align="center" wrap="wrap">
    <Flex>
      <ThemeIcon size="lg" variant="gradient" gradient={{ from: "teal", to: "lime", deg: 90 }}>
        <IconNotebook size={32} />
      </ThemeIcon>
      <Text color="green" fz="xl" fw={500} ml={5}>My Notes</Text>
    </Flex>
    <Button onClick={addNote} leftIcon={<IconFilePlus />}>Add note</Button>
    <Button.Group>
      <Button variant="light" leftIcon={<IconFileArrowLeft />}>Import</Button>
      <Button variant="light" leftIcon={<IconFileArrowRight />}>Export</Button>
    </Button.Group>
  </Flex>

  <Divider my="sm" />

  {notes.map((note, index) => (
    <Flex key={index}>
      <NavLink onClick={() => handleSelection(note.title, note.content, index)} active={index === active} label={note.title} />
      <CloseButton onClick={() => deleteNote(index)} title="Delete note" size="xl" iconSize={20} />
    </Flex>
  ))} 
</Grid.Col>

In the second column, add a toggle button to switch between edit mode and preview mode of the note. In edit mode there is a text input for the current note’s title and a text area for the current note’s content. In preview mode, the title is rendered by Mantine. Text is a component and the content is marked-reactof Markdown component:

<Grid.Col span={2}>
  <Switch label="Toggle Editor / Markdown Preview"  checked={checked} onChange={(event) => setChecked(event.currentTarget.checked)}/>

  <Divider my="sm" />

  {checked === false && (
    <div>
      <TextInput mb={5} />
      <Textarea minRows={10} />
    </div>
  )}
  {checked && (
    <Paper shadow="lg" p={10}>
      <Text fz="xl" fw={500} tt="capitalize">{title}</Text>

      <Divider my="sm" />

      <Markdown>{content}</Markdown>
    </Paper>
  )}
</Grid.Col>

Phew! It was a lot of code. The image below shows what the app looks like at this point.

App is ready to add notes

very! You can add and remove notes, but there is no way to edit them. We will add this functionality in the next section.

Add note title and content update function

Add the following code after deleteNote() function:

const updateNoteTitle = ({ target: { value } }: { target: { value: string } }) => {
  notes.splice(active, 1, { title: value, content: content })
  setTitle(value)
  setNotes([...notes])
}

const updateNoteContent = ({target: { value } }: { target: { value: string } }) => {
  notes.splice(active, 1, { title: title, content: value })
  setContent(value)
  setNotes([...notes])
}

These two functions replace the current note’s title and/or content respectively. For them to work you need to add them to your template.

<TextInput value={title} onChange={updateNoteTitle} mb={5} />
<Textarea value={content} onChange={updateNoteContent} minRows={10} />

Now, when a note is selected, its title and content will be displayed in the input text and textarea respectively. Edit the note and the title will update accordingly.

Added some notes to show how the app looks. Here are the apps with selected notes and their content.

Notes added to the app

The image below shows a preview of the note.

Note preview

The following image shows the confirmation dialog that appears while deleting a note.

Delete note dialog

very! The last thing we need to do to make our app really cool is add the ability to export and import user notes to the system hard drive.

Added note import and export functionality

Add the following code after updateNoteContent() function:

const exportNotes = async () => {
  const exportedNotes = JSON.stringify(notes)
  const filePath = await save({
    filters: [{
      name: "JSON",
      extensions: ["json"]
    }]
  })
  await writeTextFile(`${filePath}`, exportedNotes)
  sendNotification(`Your notes have been successfully saved in ${filePath} file.`)
}

const importNotes = async () => {
  const selectedFile = await open({
    filters: [{
      name: "JSON",
      extensions: ["json"]
    }]
  })
  const fileContent = await readTextFile(`${selectedFile}`)
  const importedNotes = JSON.parse(fileContent)
  setNotes(importedNotes)
}

The first function converts the note to JSON. next, save Dialog for saving notes. next, writeTextFile() A function that physically writes a file to disk. At the end, sendNotification() Ability to notify the user that the note was saved successfully and where the note was saved.

In the second function, open Use the dialog to select the JSON file containing your notes from disk.Then the file is readTextFile() The JSON content is converted to objects and finally the note storage is updated with the new content.

Finally, we need to modify the template to use the function above.

<Button variant="light" onClick={importNotes} leftIcon={<IconFileArrowLeft />}>Import</Button>
<Button variant="light" onClick={exportNotes} leftIcon={<IconFileArrowRight />}>Export</Button>

here is the final one App.tsx The file looks like this:

In the following screenshot, save as When Open Dialogs, and system notifications that appear as notes are saved.

Save As button

Notification of where files are saved

File open button

Congrats! We’ve harnessed the power of Tauri to create a fully functional note-taking desktop app.

build the app

Now, if everything worked and you are happy with the final result, you can build the app and get the operating system installation package. To do this, run the following command:

Conclusion

In this tutorial, we’ve covered what Tauri is, why it’s a better choice than Electron for building native desktop apps, and finally how to build a simple but fully functional Tauri app.

I hope you enjoyed this short trip as much as I did. To dive deeper into Tauri’s world, check out its documentation and try out its powerful features.

Related reading:

[ad_2]

Source link

What do you think?

Leave a Reply

Your email address will not be published. Required fields are marked *

GIPHY App Key not set. Please check settings

    campus city 600x400 121422

    Kessler Scholars for First Generation Students Accepting Applications: University of Dayton, Ohio

    Squire 1

    Families enjoying a holiday at Squier Farm