<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: UEDA Akira</title>
    <description>The latest articles on Forem by UEDA Akira (@akr).</description>
    <link>https://forem.com/akr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F755430%2F9843998a-a893-4fc5-a030-5cd57f7d366e.png</url>
      <title>Forem: UEDA Akira</title>
      <link>https://forem.com/akr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/akr"/>
    <language>en</language>
    <item>
      <title>Practical Guide to Developing a ChatGPT Plugin: A Case Study of Sakenowa</title>
      <dc:creator>UEDA Akira</dc:creator>
      <pubDate>Mon, 29 May 2023 09:30:00 +0000</pubDate>
      <link>https://forem.com/akr/practical-guide-to-developing-a-chatgpt-plugin-a-case-study-of-sakenowa-59a9</link>
      <guid>https://forem.com/akr/practical-guide-to-developing-a-chatgpt-plugin-a-case-study-of-sakenowa-59a9</guid>
      <description>&lt;p&gt;We've just released the Sakenowa ChatGPT plugin. It's a plugin that utilizes data from &lt;a href="https://sakenowa.com"&gt;Sakenowa&lt;/a&gt; to provide detailed information about different brands of Japanese sake and offer recommendations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--E9v4NumF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bpywvp0240bgezyp3u7u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--E9v4NumF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bpywvp0240bgezyp3u7u.png" alt="Sakenowa on the plugin store" width="800" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article shares the challenges, insights, and developer perspectives we encountered during the development of the ChatGPT plugin. We hope this can be of help to those planning to embark on their own development journey.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local Development
&lt;/h2&gt;

&lt;p&gt;During development, it's okay to deploy the API server in your local environment.&lt;/p&gt;

&lt;p&gt;In a production environment, ChatGPT's server communicates with the API server as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bLa79Z37--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fw4dzxdlzraoicjmtb9y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bLa79Z37--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fw4dzxdlzraoicjmtb9y.png" alt="Production environment" width="800" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a local environment, the browser communicates directly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--65Vq1Tts--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ehteri2kc8sad9bqvcyo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--65Vq1Tts--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ehteri2kc8sad9bqvcyo.png" alt="Local environment" width="800" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because the browser communicates with the API server from the ChatGPT page (&lt;code&gt;chat.openai.com&lt;/code&gt;), it is necessary to set up CORS on the API server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication
&lt;/h2&gt;

&lt;p&gt;The authentication method for the API server is specified in the manifest file (&lt;code&gt;ai-plugin.json&lt;/code&gt;). In the Sakenowa ChatGPT plugin, we used &lt;code&gt;service_http&lt;/code&gt;. A pre-registered key is set as a &lt;code&gt;Bearer&lt;/code&gt; token in the &lt;code&gt;Authorization&lt;/code&gt; header, which you can confirm on the API side.&lt;/p&gt;

&lt;p&gt;Example of an &lt;code&gt;Authorization&lt;/code&gt; header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authorization: Bearer &amp;lt;API-KEY&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is registered during the plugin application process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manifest File
&lt;/h2&gt;

&lt;p&gt;The manifest file (&lt;code&gt;ai-plugin.json&lt;/code&gt;) needs to be deployed as &lt;code&gt;/.well-known/ai-plugin.json&lt;/code&gt; on the public server.&lt;/p&gt;

&lt;p&gt;There are people who have compiled various plugin manifest files and API specification files: &lt;a href="https://github.com/sisbell/chatgpt-plugin-store"&gt;https://github.com/sisbell/chatgpt-plugin-store&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As can be seen here, most plugins are accessible to anyone. However, as long as the plugin can be accessed from the ChatGPT server, it's sufficient, and access can be restricted. The range of IP addresses used by ChatGPT's server is listed in the ChatGPT plugin documentation.&lt;/p&gt;

&lt;p&gt;According to ChatGPT's Plugin devtools, the &lt;code&gt;description_for_model&lt;/code&gt; value in the manifest file is embedded directly into the prompt, so there's little point in hiding it. There's no guarantee that other parts of the manifest file won't be embedded in the future. You should only include information in the manifest file that you are willing to make public.&lt;/p&gt;

&lt;p&gt;By the way, the part embedded in the prompt can be answered by asking ChatGPT. The Sakenowa ChatGPT plugin doesn't publicly release its manifest file, but ChatGPT can provide the information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lJkDQ11m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ruzqficu608gn3jww69w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lJkDQ11m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ruzqficu608gn3jww69w.png" alt="description of the plugin" width="800" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  API Specification File
&lt;/h2&gt;

&lt;p&gt;According to ChatGPT Plugin devtools, the contents of the API specification file are converted to TypeScript and embedded into the prompt. For the Sakenowa ChatGPT plugin, the content is embedded as follows (slightly abbreviated):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nx"&gt;sakenowa&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="c1"&gt;// Search brands by query&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;search__brands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="c1"&gt;// query string that will match with brand name. Can be Japanese name (Kanji or Hiragana), English name and also part of them.&lt;/span&gt;
&lt;span class="nl"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="c1"&gt;// search by area. It should be official Japanese prefecture numbers&lt;/span&gt;
&lt;span class="nx"&gt;area&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// namespace sakenowa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the return type is defined in the API specification file and should be an essential piece of information for selecting the optimal endpoint, it is set to &lt;code&gt;any&lt;/code&gt; here. Even though array length limitations are specified in the API specification file, they don't appear here. However, the &lt;code&gt;description&lt;/code&gt; values seem to be output as comments, so it may be beneficial to write information here.&lt;/p&gt;

&lt;p&gt;This format suggests that information like the API server's URL won't be embedded in the prompt. But, as the future is uncertain, you need to treat it as potentially public. Also, access can be restricted to only ChatGPT's server by specifying an IP address range.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MOfbWoTW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ydsaljp5gq3tw6lft302.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MOfbWoTW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ydsaljp5gq3tw6lft302.png" alt="API spec" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Public! How to Get Users?
&lt;/h2&gt;

&lt;p&gt;As of May 29, 2023, the review process is stated to be completed within seven days after application. The Sakenowa ChatGPT plugin took exactly seven days.&lt;/p&gt;

&lt;p&gt;After going public, we issued a press release. Typically, for a smartphone app, we would provide the store's URL for users to access it directly. However, the ChatGPT plugin store does not have a URL. Additionally, since there is no search function, users will need to locate the plugin from a list that spans approximately 20 pages. We anticipate improvements in this area soon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-verification
&lt;/h2&gt;

&lt;p&gt;If you change the manifest file after the plugin has been approved and listed in the store, it will be removed from the store and re-verification will be required. From what I've heard, the current situation is as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Even if the plugin is removed from the store, users who are already using it can continue to do so.&lt;/li&gt;
&lt;li&gt;The time it takes for re-verification varies, sometimes it's three days, but other times it can take up to a week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This issue is being discussed in OpenAI's forum, so I believe it will be improved eventually. However, for now, you need to understand that changing the manifest file after release is a challenging task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The official documentation for ChatGPT plugin development is well-written, so you're unlikely to struggle due to a lack of information. However, there are difficulties that developers may encounter, such as the re-verification process after going public.&lt;/p&gt;

&lt;p&gt;In this article, I've shared knowledge and experience gained during the development of the Sakenowa ChatGPT plugin. I hope this can serve as a supplement to the official documentation, and prove useful for those about to develop a ChatGPT plugin.&lt;/p&gt;

</description>
      <category>chatgpt</category>
      <category>sakenowa</category>
    </item>
    <item>
      <title>8 Tips for Creating a Native Look and Feel in Tauri Applications</title>
      <dc:creator>UEDA Akira</dc:creator>
      <pubDate>Fri, 07 Oct 2022 11:31:25 +0000</pubDate>
      <link>https://forem.com/akr/8-tips-for-creating-a-native-look-and-feel-in-tauri-applications-3loe</link>
      <guid>https://forem.com/akr/8-tips-for-creating-a-native-look-and-feel-in-tauri-applications-3loe</guid>
      <description>&lt;p&gt;I recently released &lt;a href="https://jomai.app" rel="noopener noreferrer"&gt;Jomai&lt;/a&gt;, a Markdown-focused desktop search app made in Tauri.&lt;br&gt;
Apps made with Tauri is, of course, native app. But its UI part runs on WebView, so its look and feel tend to be like a web app. I designed Jomai to behave as much as possible like a native app, so I will summarize that here.&lt;br&gt;
Jomai is currently available only for macOS, so this article is also intended for macOS.&lt;/p&gt;

&lt;p&gt;Environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tauri: 1.1.1&lt;/li&gt;
&lt;li&gt;Platform: macOS&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Get rid of the beep sound on keystrokes for non-input &lt;/li&gt;
&lt;li&gt;Make text in UI not selectable&lt;/li&gt;
&lt;li&gt;Set mouse cursor to default&lt;/li&gt;
&lt;li&gt;Do not scroll the entire screen&lt;/li&gt;
&lt;li&gt;Suppress bounce scrolling across the screen&lt;/li&gt;
&lt;li&gt;Prepare your own menu&lt;/li&gt;
&lt;li&gt;Prepare your own keyboard shortcuts&lt;/li&gt;
&lt;li&gt;Support dark mode&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Get rid of the beep sound on keystrokes for non-input fields &lt;a id="item-1"&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;A beep sounds when keystrokes are made while the focus is on a non-input item other than &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;. This issue occurs in WebView on macOS and has a GitHub issue, which has not been resolved as of October 6, 2022.&lt;br&gt;
&lt;a href="https://github.com/tauri-apps/tauri/issues/2626" rel="noopener noreferrer"&gt;https://github.com/tauri-apps/tauri/issues/2626&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can suppress the beep sound by calling &lt;code&gt;preventDefault()&lt;/code&gt; on &lt;code&gt;keydown&lt;/code&gt; event, but this will also disable all input to &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, etc., so it is necessary to suppress it selectively.&lt;/p&gt;

&lt;p&gt;Jomai uses the following code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UseKeyCallback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onKeydown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyboardEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;　　　&lt;/span&gt; &lt;span class="c1"&gt;// callback should return true if processed&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;consumed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctrlKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shiftKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;composedPath&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;HTMLAreaElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;consumed&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// preventDefault() if it is not an input item&lt;/span&gt;
        &lt;span class="c1"&gt;// also preventDefault() if it was handled by a callback&lt;/span&gt;
        &lt;span class="nf"&gt;applyBeepSoundWorkaround&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onKeydown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onKeydown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;onKeydown&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It solves most cases of the problem to call &lt;code&gt;preventDefault()&lt;/code&gt; if the event source is not an &lt;code&gt;HTMLInputElement&lt;/code&gt; or &lt;code&gt;HTMLAreaEelement&lt;/code&gt;. However, when the focus is on one of these input items and the keyboard shortcut is used to move the focus to a non-input item (yes, it isn't very easy), there will be the sound. So I've done more to fix the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make text in UI not selectable &lt;a id="item-2"&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;While it is normal to be able to select text on a web page, this is generally not the case in native apps. To avoid unintentional text selection when trying to manipulate the UI, use &lt;code&gt;user-select: none&lt;/code&gt; at the base of DOM elements such as &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;user-select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Set mouse cursor to &lt;code&gt;default&lt;/code&gt; &lt;a id="item-3"&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Since the previous section has limited the operation of UI elements to button pressing, etc., let's express it that way for the mouse cursor as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cursor: default&lt;/code&gt; prevents I-beam when hovering text, for example. The &lt;code&gt;cursor&lt;/code&gt; attribute should be explicitly specified if the UI element is manipulatable, such as a button.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Do not scroll the entire screen &lt;a id="item-4"&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;When content is larger than the size of the screen, it's usual for web pages to scroll the entire screen. In native apps, it is natural to display fixed UI components and make only some scrollable.&lt;/p&gt;

&lt;p&gt;In Jomai, tabs and forms at the top of the screen are always visible, and only the content part at the bottom is scrollable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcpxn382l39cq70z25u8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcpxn382l39cq70z25u8.png" alt="scrollable and non-scarollable areas in the app"&gt;&lt;/a&gt;&lt;br&gt;
See it in action.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyheu6wni6r57dzerhltx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyheu6wni6r57dzerhltx.gif" alt="only contents area is scrollable"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To achieve this, set &lt;code&gt;overflow: hidden&lt;/code&gt; for the entire screen, and set &lt;code&gt;overflow: scroll&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; for the scrollable area.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.page&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.contents&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;scroll&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100vh&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="m"&gt;40px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Suppress bounce scrolling across the screen &lt;a id="item-5"&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Bounce scrolling is that thing that makes a spring-like motion when scrolling to the edge of the screen. By default, the entire screen is scrollable with the bounce motion. See this video.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftae52qpjh4f0kbtiu1ai.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftae52qpjh4f0kbtiu1ai.gif" alt="bounce scroll"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bounce scrolling is associated with scrollability, so it should be suppressed if the entire screen should not be scrollable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prepare your own menu &lt;a id="item-6"&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Tauri provides a basic menu by default.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl6e7szlvm6jnze09o5dv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl6e7szlvm6jnze09o5dv.png" alt="default menus"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you need customization, you will need to build these menus yourself, which is a bit tedious. See &lt;code&gt;tauri::Menu::os_default&lt;/code&gt; for reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prepare your own keyboard shortcuts &lt;a id="item-7"&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Be careful when using your own keyboard shortcuts on web pages, as they can confuse users, but be proactive about introducing them in native apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Support dark mode &lt;a id="item-8"&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;I am a fan of dark mode and would like to see dark mode support in apps I use regularly.&lt;br&gt;
Tauri has &lt;code&gt;appWindow.theme()&lt;/code&gt; to get the current theme and &lt;code&gt;appWindow.onThemeChanged()&lt;/code&gt; to monitor change events. Using these, you can change the app to match the OS theme settings.&lt;/p&gt;

&lt;p&gt;Here is an example implementation in React.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;appWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Theme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tauri-apps/api/window&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;unlisten&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UnlistenFn&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;appWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

      &lt;span class="nx"&gt;unlisten&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;appWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onThemeChanged&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`theme changed to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;})();&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unlisten&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;unlisten&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this article, I have introduced some tips to make Tauri-made apps more native-like. A little effort will improve the usability of your apps. I hope you find them helpful.&lt;/p&gt;

&lt;p&gt;Overall, developing with Tauri has been a great experience, and I look forward to creating many more apps with Tauri.&lt;/p&gt;

&lt;p&gt;Please also check out &lt;a href="https://jomai.app" rel="noopener noreferrer"&gt;Jomai&lt;/a&gt;, which I'm developing using these techniques.&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>react</category>
      <category>webview</category>
    </item>
  </channel>
</rss>
