Coding in Delphi and Beyond - Thoughts from David Cornelius https://corneliusconcepts.tech/ en Code Better in Delphi - by Alister Christie https://corneliusconcepts.tech/code-better-delphi-alister-christie <span property="dc:title">Code Better in Delphi - by Alister Christie</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2023-07-18T05:36:39+00:00" datatype="xsd:dateTime">Mon, 07/17/2023 - 22:36</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>The author of <a href="https://leanpub.com/codefasterindelphi">Code Faster in Delphi</a> has just released his second book, <a href="https://learndelphi.tv/codebetter">Code Better in Delphi</a>--and it is just as packed with useful coding techniques that will improve the way you write software as the first. The book is an easy read with short, clear examples that illustrate the concepts presented. It covers coding, analysis, design paradigms, instructs on the use of both Delphi IDE capabilities and external tools, offers advise on working with teams and provides many resources throughout for deeper research and learning.</p> <p>His writing is based on experience that many of us have witnessed first-hand: inheriting a set of code fraught with nested <code>with</code> statements, extremely long procedures, obtuse identifier names, and unreachable code left to confuse the next coder. I found myself nodding in agreement at times and shuddering with recent memory at others while reading; but every tale of horror was followed with hints on how to improve the code without breaking it.</p> <h3>Part 1 - Coding Basics</h3> <p>I read the first of the four parts (about a quarter of the 224-page book) in about an hour. In addition to code improvement tips are instructions on how to put these techniques to use immediately using the Delphi IDE's code formatting options, refactoring features, and included tools.</p> <p>I believe the professional programmer would do well to heed his advice on coding style, learn the SOLID principles, and study the design patterns he reviews. Software teams that implement standard policies based on these guidelines would produce more maintainable code, new programmers joining the team would get up to speed quickly, and debugging time would be decreased. I found his explanations on why these principles are important to be a fresh, new perspective I appreciated very much.</p> <h3>Part 2 - Tools</h3> <p>The second part of the book gets into utilizing Delphi and related tools, libraries, and plug-ins to provide valuable insights to your code. I was glad to see this as writing good code is only the start--analyzing what you've written and using aids to improve are great motivators for betterment. Alister thoroughly covers the add-ons every serious Delphi programmer should have in their toolkit.</p> <h3>Part 3 - Working with Code</h3> <p>Part three is the largest section--and covers topics often ignored or shoved down the priority list so far, it never happens: testing, debugging, and cleaning your code! After explaining the different types of testing (functional, integration, unit, and TDD), he takes a deep dive into debugging tips and some intricacies of the Delphi debugger that elude many programmers. Debugging threads, setting up watch groups, and using the Modules, Events, and CPU windows effectively are all covered in simple terms. </p> <p>Also discussed here is larger coverage of the topic of dependency injection, introduced in the first part of the book. In addition to describing what it is and how it works, he contrasts what it isn't and where it shouldn't be used. This helps clarify it's true purpose instead of leading to the presumption that <em>every object</em> needs to be interfaced. Again, his clear and simple examples inspire the reader to dig deeper--and he provides external resources for doing just that.</p> <p>The advise on cleaning your code was not only explained with clear examples and an explanation of tools to help, but also nicely ties in with the previous sections of this part of the book by describing a common function of many applications, logging, and how to use dependency injection to remove global objects and make your code more testable in the process.</p> <h3>Part 4 - Beyond Delphi</h3> <p>If you have never used version control, you should read Alister's introduction of the subject which briefly describes the concepts and highlights a couple of the popular systems in use today and how they can be used to enhance collaboration. The discussion broadens from there to cover software development methods and processes beyond managing your code to managing projects and teams, discussing Waterfall, Agile, and Kanban methodologies. But he doesn't stop there; the book finishes by touching on continuous integration, working remotely, and backups, giving the Delphi programmer many things to consider when pursuing the goal of better coding.</p> <h3>Conclusion</h3> <p>This book starts off doing exactly what the title proposes: showing you how to <em>Code Better in Delphi</em>. But the author realizes that a programmer is more than just the what is written between <code>begin</code> and <code>end</code>. Design and analysis are important to understand what you are supposed to do (design) and how well you did it (analysis); and if there's a error, how to quickly find it (debugging). These parts, I expected. But I was delighted to find the additional coverage of how your code and your team are managed--all affecting how to code better overall.</p> <p>Software development is a continual learning process; there are always new tools and techniques, languages and libraries, designs and demands. Keeping up with it all is a challenge. Fortunately, there are authors like Alister Christie who provide readable books and <a href="https://www.youtube.com/@codegearguru">helpful videos</a> that pack a lot of useful information in a compact and approachable manner to help. I learned something in every section (and I've been using Delphi since it first came out!).</p> <p>I highly recommend this book to every Delphi programmer--new and experienced!</p> </div> <section id="node-comment"> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=176&amp;2=comment_node_blog&amp;3=comment_node_blog" token="OFGilZjl1FdGVxk-heaVeJUq6ZtL57RxFDcd1PNOP7Q"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Tue, 18 Jul 2023 05:36:39 +0000 david 176 at https://corneliusconcepts.tech https://corneliusconcepts.tech/code-better-delphi-alister-christie#comments Ubuntu Auto-Login https://corneliusconcepts.tech/ubuntu-auto-login <span property="dc:title">Ubuntu Auto-Login</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/linux" hreflang="en">Linux</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2023-06-25T06:26:25+00:00" datatype="xsd:dateTime">Sat, 06/24/2023 - 23:26</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>A few months ago, I <a href="/automating-hugo-github-actions-raspberry-pi" rel="nofollow">blogged</a> about getting a Raspberry Pi set up with a GitHub Runner to keep a website up-to-date. It's been working well--until today. I made some updates, pushed them to GitHub, then later checked the website--it had not updated.</p> <p>Then it dawned on me: the power had gone out for a couple of hours last week and even though the computer was back on, I had not logged in and started the GitHub runner--and had forgotten to make that automatic.</p> <p>Setting up Linux to login and auto-start a program isn't terribly difficult but the commands are a little different depending on which distribution and version of Linux you're running. I have <strong>Ubuntu Server 22.04</strong> running and found what I needed on <a href="https://askubuntu.com/questions/1410997/auto-login-on-tty4-or-tty5" rel="nofollow">AskUbuntu.com</a> to automatically login on a the terminal:</p> <ol> <li>Create a directory for the terminal you want to use if it doesn't already exist:<br /> <code>mkdir -p /etc/systemd/system/getty@tty1.service.d</code></li> <li>Create an <strong>autologin.conf</strong> file and specify the <code>--autologin</code> parameter with the user desired:<br /> <code>[Service]<br /> ExecStart=<br /> ExecStart=-/sbin/agetty --noclear -n --autologin david %I $TERM</code></li> </ol> <p>Once I rebooted the machine to make sure that worked, I needed to automatically start the GitHub runner. This was a simple add-on to the <strong>.profile</strong> file in my user's home directory:</p> <ul> <li><code>/mnt/webdisk/actions-runner/run.sh</code></li> </ul> <p>This final piece of automation for my web-builder machine is now in place. :-)</p> <p> </p></div> <section id="node-comment"> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=175&amp;2=comment_node_blog&amp;3=comment_node_blog" token="pcoU_oajpPpO4NbOALOQBMoIWXxgI-4-MeZCGAUTHm8"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Sun, 25 Jun 2023 06:26:25 +0000 david 175 at https://corneliusconcepts.tech https://corneliusconcepts.tech/ubuntu-auto-login#comments Creating a Delphi Welcome Page Plug-in https://corneliusconcepts.tech/creating-delphi-welcome-page-plug <span property="dc:title">Creating a Delphi Welcome Page Plug-in</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2023-04-19T05:20:12+00:00" datatype="xsd:dateTime">Tue, 04/18/2023 - 22:20</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>There have been several iterations of the Delphi IDE's Welcome Page, or the first page that you see by default when you start Delphi without a project. This space has been used for keeping a list of favorite projects for convenient loading, marketing of related products by Embarcadero, and quick links to tutorials and resources. Delphi 11 introduced, in my opinion, the best interface yet as it's completely customizable by the user and provides an API for writing your own plug-ins to provide additional functionality.<br /> <br /> There is not much documentation on how to use this API except for reading code comments while wading through the source in <code>ToolsAPI.WelcomePage.pas</code>; those help but you don't really learn how to use the functions available until you dig into the sample Welcome Page plug-ins that come with Delphi. I had a real business reason to do that recently and am sharing what I learned here in hopes it helps others get started.</p> <p>There are three sample projects installed under the <code>DEMOSDIR</code> folder (<code>\Samples\Object Pascal\VCL\VCL WelcomePage</code>):</p> <ul> <li><strong>WP.BasicDemoPlugin</strong> - a simple frame with static text.</li> <li><strong>WP.CaptionFrameDemoPlugin</strong> - a more useful frame showing some system information and presented with a standard "plug-in caption" frame created by the plug-in framework.</li> <li><strong>WP.DemoPlugin</strong> - a ListView filled with the currently installed plug-ins and demonstrating interactivity, setting favorites, and clicking links; this one also creates its own "plugin caption" frame.</li> </ul> <h2>The Fundamentals</h2> <p>All Welcome Page plug-ins are design-time packages using the VCL framework. One global procedure named <code>register</code> is called to create the plug-in class and register it. It does so by calling <code>WelcomePagePluginService.RegisterPluginCreator</code> which takes a constructor to your class as its parameter. Your plug-in class must be a descendant of <code>TInterfacedObject</code> and implement both the <code>INTAWelcomePagePlugin</code> and <code>INTAWelcomePageContentPluginCreator</code> interfaces.</p> <h3>INTAWelcomePagePlugin</h3> <p>This simple interface provides three functions:</p> <ul> <li><code>GetPluginID</code>: the unique ID by which your plug-in is referenced.</li> <li><code>GetPluginName</code>: the friendly name that shows in the list of "Available Plugins" and standard caption forms.</li> <li><code>GetPluginVisibile</code>: this Boolean function determines whether your plug-in is immediately showing when installed (True) or if it simply appears in the list of "Available Plugins" (False).</li> </ul> <h3>INTAWelcomePageContentPluginCreator</h3> <p>This interface tells the plug-in system what your plug-in will look like and how to create it by declaring the the functions <code>CreateView</code> and <code>GetView</code>, both returning a <code>TFrame</code>. Typically, your class will define a private variable to keep a handle to your frame, <code>GetView</code> will simply return that variable and <code>CreateView</code> will create and initialize it.</p> <p>The frame you create must be a <code>TFrame</code> descendant; you can define it at design time in a separate unit, much like you would design any other form or frame for VCL applications, with controls and interactivity and logic. You do have to remember it's functionality is acting within the Delph IDE and keep in mind the constraints this implies.</p> <p>The first two sample plug-ins create simple frames.</p> <p>Instead of creating a <code>TFrame</code> descendant yourself, you can call <code>WelcomePagePluginService.CreateListViewFrame</code> to create a ListView frame. If you do that, you also have to create two other classes that define how and when the ListView gets loaded and the data structure for each of the items in the ListView.</p> <ul> </ul> <h2>WP.BasicDemoPlugin</h2> <p>The simplest of the sample plug-ins is nothing more than a simple VCL <code>TFrame</code> with a label of static text placed over a panel aligned to <code>alClient</code>. It introduces the concept of registering a plugin and shows how to tie into the current IDE theme by setting the panel's <code>BackgroundColor</code> in the frame's constructor.</p> <h2>WP.CaptionFrameDemoPlugin</h2> <p>The next sample plug-in is very similar to the first in that it's still a simple <code>TFrame</code> descendant with static text but the text is filled with system information in the constructor, like computer name, the amount of memory in the computer, the version of Windows, and so forth.</p> <p>The interesting part is the introduction of a "Caption Frame" in which our defined <code>TFrame</code> descendant is placed. The first plug-in did not define either a standard or custom "caption panel" so when displayed in the IDE, all you see is a bare panel with text; this one now displays the plug-in name in a standard caption area above the main plug-in view frame. It does this in the <code>CreateView</code> function with a call to <code>WelcomePagePluginService.CreateCaptionFrame</code>. The result of that function call becomes the result of the function and the owner of the custom frame containing the static text; additionally, it also has to call <code>SetClientFrame</code> to associate the frame view with the caption frame.</p> <h2>WP.DemoPlugin</h2> <p>The third sample Welcome Page plug-in differs significantly from the first two in that the main view of the plug-in is not a simple <code>TFrame</code> but is a ListView created by the Welcome Page framework. In order to use this, the plug-in must define how the ListView is filled by declaring two classes, a "model" class and an "item data" class. This demo plug-in also provides a caption area for displaying the name by creating a custom <code>TFrame</code> and placing the plug-in name on a label along with a couple of buttons.</p> <h3>Responding to IDE Theme Changes</h3> <p>One thing I noticed with the first two plugins that implemented simple frame-based views, the color scheme of the IDE is only checked during creation of the frame. If the user changes the theme from light to dark (or vice versa), the Embarcadero Welcome Page plug-ins respond and repaint themselves with the new colors but the first two plug-ins do not. If you re-install the plug-ins or restart Delphi, they'll pick up the changed color theme and color their frames accordingly during their creation but they're stuck with that throughout the life of the plug-in.</p> <p>The ListView-based plug-in <em>does</em> respond to theme changes: the ListView colors are managed by the Welcome Page framework; the custom caption frame overrides the <code>PaintWindow</code> procedure and utilizes <code>BorlandIDEServices</code> to get the system color and then paints the frame's background with <code>WelcomePagePluginService</code>.</p> <h3>ListView Data</h3> <p>The model class manages loading and refreshing data, setting icons, and handling click events. This class inherits from <code>TInterfacedObject</code> and must implement the <code>INTAWelcomePagePluginModel</code> interface.</p> <p>The item data class defines the storage to hold the title, description, and optional icons for each item in the plug-in's ListView. It inherits from <code>TInterfacedObject</code> and must implement the <code>INTAWelcomePageModelItemData</code> interface; it can also optionally implement the <code>INTAWelcomePageModelItemAdditionalData</code> interface--which the demo plug-in does in order to display a favorite icon.</p> <h3>INTAWelcomePagePluginModel</h3> <p>This interface defines functions and methods for managing data in the ListView:</p> <ul> <li><code>LoadData</code>: implement this procedure to fill the ListView.</li> <li><code>RefreshData</code>: implement this procedure to refresh the ListView.</li> <li><code>StopLoading</code>: implement this procedure to allow the IDE to abort a long loading process.</li> <li><code>SetOnLoadingFinished</code>: specify the call-back procedure that will get called after the data is loaded.</li> <li><code>GetData</code>: this function should return a pointer to an <code>IInterfaceList</code> of items in the ListView; the demo plug-in does this with the private field, <code>FData</code>.</li> <li><code>GetStatusMessage</code>: this function returns a simple status string; often used to report error messages during data loading.</li> <li><code>GetIsDataLoaded</code>: a function indicating whether data has finished loading.</li> <li><code>GetImageCollection</code>: a function returning a <code>TImageCollection</code> of images linked to items in the ListView; the demo plug-in project includes a "resources" data module containing the collection of images.</li> </ul> <p>The demo plug-in shows a list of installed plug-ins in the Delphi IDE; its <code>LoadData</code> procedure calls another <code>WelcomePagePluginService</code> to get the list of plug-ins and adds the name and description of each to the ListView.</p> <h3>INTAWelcomePageModelItemData</h3> <p>This interface defines the "getter" and "setter" methods for the title, description, and image index of the items in the ListView:</p> <ul> <li><code>GetTitle</code>/<code>SetTitle</code></li> <li><code>GetDescription</code>/<code>SetDescription</code></li> <li><code>GetImageIndex</code>/<code>SetImageIndex</code></li> </ul> <p>The demo plug-in sets the title and description to the name of each of the installed plug-ins it finds and if the name of the plug-in starts with "Embarcadero" sets the ImageIndex to a special icon.</p> <h3>INTAWelcomePageModelItemAdditionalData</h3> <p>This optional interface provides the "getter" and "setter" methods for the ListView items' right-side button icon:</p> <ul> <li><code>GetAdditionalImageIndex</code>/<code>SetAdditionalImageIndex</code></li> </ul> <p>If this interface is not included, the ListView items don't show the additional button. The demo plug-in uses this to display a "favorite" icon--but it's for demonstration purposes only as the list of favorites is not stored.</p> <h2>ProjectTreePlugin</h2> <p>The company I work for has hundreds of small Delphi projects with overlapping functionality dating back over three decades and originally written in COBOL. The file naming structure they developed dictates that the first two characters of the project name determine the "application module" to which it belongs. All these projects are in the same source folder. So, a useful the IDE plug-in would be a TreeView listing all the projects in a source folder grouped by the first two characters of the project name.</p> <p>With this goal in mind, I copied the "CaptionFrameDemoPlugin" project to a new folder and modified it to show all projects from a configured source folder in a TreeView; thus, <a href="https://github.com/corneliusdavid/WP.ProjectTreePlugin"><code>WP.ProjectTreePlugin</code></a> was born.</p> <p>The plug-in provides a simple frame to house the TreeView and implements the standard caption frame created by the <code>WelcomePagePluginService</code>. Just above the TreeView is a small panel containing a label and a button. The label displays the currently configured source folder and the button allows the user to select a folder. The TreeView is filled upon the construction or after the folder is selected.</p> <p>The new and interesting parts of this plug-in are storing and retrieving a configuration setting, and loading a project into the IDE when an item is double-clicked in the TreeView.</p> <h3>Welcome Page Configuration Settings</h3> <p>While looking through the <code>ToolsAPI.WelcomePage</code> unit, I stumbled across a promising global variable: <code>WelcomePageSettings: INTAWelcomePageSettings</code>. After a little experimentation, I found it can be used by your plug-in to conveniently store configuration data for your Welcome Page plug-in. I used the <code>IsSettingExists</code> function to check if the source folder has been configured and if so, calls <code>ReadSetting</code> to get the folder and load the TreeView with projects found therein. When the button is clicked and a new folder is selected, <code>SaveSetting</code> is called to save the folder.</p> <p>The configuration data is stored in the Windows registry under the current user's <code>SOFTWARE\Embarcadero\BDS\22.0\WelcomePage</code> key. All plug-ins using this method of saving configuration data will be placed in the same registry key so make sure your plugin value identifiers are unique--or save them yourself elsewhere.</p> <h3>Loading a Delphi Project</h3> <p>One thing I figured a sample plug-in project would certainly demonstrate is how to load a project into the IDE. Surprisingly, that was something I had to figure out myself. Once again, though, reading through Embarcadero's source gave me what what I needed.</p> <p>If you search for "OpenProject" in the <code>ToolsAPI</code> unit, you'll come across the <code>IOTAActionServices</code> interface which provides the following useful functions:</p> <ul> <li><code>OpenProject</code></li> <li><code>OpenFile</code></li> <li><code>ReloadFile</code></li> <li><code>SaveFile</code></li> <li><code>CloseFile</code></li> </ul> <p>Near the bottom of that unit is the global variable, <code>BorlandIDEServices</code> which provides access to all the <code>IxxxxServices</code> interfaces with an example of how to use it.</p> <p>Armed with that information, I was able to hook into the <code>OnDblClick</code> event of the plug-in's TreeView, <code>tvProjectTree</code>, and open the selected project with the following code:</p> <pre> if Supports(BorlandIDEServices, IOTAActionServices, ModuleServices) then begin (BorlandIDEServices as IOTAActionServices) .OpenProject(TPath.Combine(ProjPath, tvProjectTree.Selected.Text + '.dpr'), True); end; </pre> <p>If you'd like to see this entire project, it's been made <a href="https://github.com/corneliusdavid/WP.ProjectTreePlugin">open source on GitHub</a>--and has a nice <a href="https://github.com/corneliusdavid/WP.ProjectTreePlugin/blob/main/ProjTree.png">screenshot</a>.</p> <h2>Future Ideas</h2> <p>I got this to work fairly well but since, like I mentioned, there are hundreds of projects, there's a little delay whenever the plug-in is reinstalled or when Delphi starts up and is initializing the Welcome Page plug-ins. The projects are all stored on a fast local SSD so loading time is minimal but it would be a good idea to make this multi-threaded for smoother operation.</p> <p>Another idea is to use a ListView instead of a TreeView and provide a dropdown above the ListView as the list of "application modules" that filter the list of projects in the ListView. With that approach, it would lend itself more for general use where the drop-down list of folders could be a configurable list of "favorite" folders. That may seem duplicitous with Delphi's project list of marked favorites surfaced to the top but if you work on dozens or hundreds of projects that list soon gets unmanageably large.</p> <h2>Conclusion</h2> <p>I learned a lot while exploring these plug-ins and writing my own--and I had fun doing it! There's a lot more you can do--I hope this introduction gives you a head start for building your own useful IDE Welcome Page Plugins.</p> <p>Now, I need to find an excuse to dig into another new API, the <a href="https://docwiki.embarcadero.com/RADStudio/Alexandria/en/11_Alexandria_-_Release_3#ToolsAPI_support_for_the_Code_Editor">ToolsAPI.Editor</a>!</p> </div> <section id="node-comment"> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=174&amp;2=comment_node_blog&amp;3=comment_node_blog" token="Ulq3gSiF72gC3dfwhIwwrl43O-fIhQudTrqHEZneb58"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Wed, 19 Apr 2023 05:20:12 +0000 david 174 at https://corneliusconcepts.tech https://corneliusconcepts.tech/creating-delphi-welcome-page-plug#comments Automating Hugo with GitHub Actions on a Raspberry Pi https://corneliusconcepts.tech/automating-hugo-github-actions-raspberry-pi <span property="dc:title">Automating Hugo with GitHub Actions on a Raspberry Pi</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/linux" hreflang="en">Linux</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2023-03-30T06:06:39+00:00" datatype="xsd:dateTime">Wed, 03/29/2023 - 23:06</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>Over the last few months, I've been studying and working on getting a website up with <a href="https://gohugo.io">Hugo</a>, a static site builder based on Markdown files. It doesn't take months to learn Hugo--it's pretty straight-forward and there's a great <a href="https://www.youtube.com/watch?v=qtIqKaDlqXo&amp;list=PLLAZ4kZ9dFpOnyRlyS-liKL5ReHDcj4G3">YouTube video course</a> that gets you up to speed quickly. But I also <a href="https://corneliusconcepts.tech/go">took time to learn the Go</a> programming language in which Hugo is written and built a program, first in Go then in Delphi (and <a href="https://odug.org/2023-03">shared at ODUG</a>), to build content files more efficiently than hand-editing. Now, as the content files are built and website assets are put in place, I need Hugo to build the entire site locally and upload the generated HTML/JS/CSS files to the website whenever there's new content. Naturally, I wanted to automate this as much as possible.</p> <p>The files Hugo needs are stored in a repository on GitHub which offers something called <a href="https://docs.github.com/en/actions">GitHub Actions</a>. GitHub Actions are triggered by certain events to launch pre-defined processes--such as rebuilding and uploading files for a website. The trigger in my case needs to be any push of new commits to the repository. When that happens, the GitHub Action launches a GitHub <strong>Runner</strong> and processes steps in a defined <strong>Workflow</strong>. </p> <p>You can use GitHub's standard runners that spin up containerized virtual machines, download your repository, install prerequisite tools and libraries, build and deploy your app or website, then destroy itself; or you can use your own self-hosted runners installed and running on your own hardware. Using GitHub's runners will incur charges after a certain amount of free monthly minutes are used and since I'm building a personal photo album with thousands of pictures and frequent updates, I know it would quickly exceed <a href="https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration">GitHub's usage limits</a>. So I dove in the deep end to figure out how to configure Actions and Workflows and set up my own self-hosted Action Runner.</p> <p>This was quite a learning curve but after reading lots of documentation and blogs and wading through some trial-and-error, I got it working on an old Linux box I had sitting in the corner of my office. Once the concept was proven, I decided to move everything to a Raspberry Pi, partially to save space (I wanted to get rid of spare computers sitting in office corners) and because I wanted to see if it could be done. I took notes along the way and now that it's done and working, I wanted to officially document the steps in case I ever need to do this again--and in case anyone else is interested in what's involved.</p> <h3>Selecting the OS</h3> <p>The operating system of choice for a Raspberry Pi is often <a href="https://www.raspbian.org/">Raspbian</a> because it has fairly low resources and provides the basics most users would like to have to get started exploring their small Linux device. But getting a GitHub Actions-Runner installed on Raspbian is problematic--it's not impossible if you understand how to manually install special versions of .NET core, but I didn't want to spend that much time if there was a simpler path. Also, once set up, I won't need a GUI at all which would take extra CPU and memory, precious resources on a Raspberry Pi.</p> <p>So, I chose <a href="https://ubuntu.com/download/server#downloads">Ubuntu Server</a>. Ubuntu is very popular and thus is supported by a wide variety of applications, including <a href="https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners#supported-architectures-and-operating-systems-for-self-hosted-runners">self-hosted GitHub runners</a>; plus, I don't need a GUI as command-line driven, automated processes don't need to be constantly monitored once they're up and running (besides, the GitHub Action web interface has a pretty good real-time view of actions being processed, I found out). For my Raspberry Pi 3, I used the 64-bit ARM version and with the handy <a href="https://www.raspberrypi.com/news/raspberry-pi-imager-imaging-utility/">Raspberry Pi Imager</a>, soon had a bootable microSD card ready. </p> <h3>Setting up the User</h3> <p>Once booted up on the Pi, I logged in as the default user; for a new install of Ubuntu, the username is just <code>ubuntu</code>. I could've stuck with that but I wanted to use a personal login so created my own account while logged in as the initial <code>ubuntu</code> account:</p> <p><code>sudo adduser david</code></p> <p>Since I will be needing to run commands as a root user and since, by default, new users are not trusted, I needed to add my new user account to the list of users that can run <code>sudo</code>:</p> <p><code>sudo usermod -aG sudo david</code></p> <p>Again, these steps are completely optional but I wanted to make it consistent with other Linux environments I have.</p> <h3>Attaching an External Drive</h3> <p>The amount and size of files I will be dealing with will quickly exceed the available space on the 64-GB SD card I am using to boot the Raspberry Pi so I attached a 500 GB external drive via USB. To get this to be automatically mounted each time the Pi is started, I had to add an entry to the /etc/fstab file. I won't go into all the details of how to do this because I followed the well-written instructions I found at this site: <a href="https://www.howtogeek.com/444814/how-to-write-an-fstab-file-on-linux/">How to Write an fstab File on Linux</a>. One word of warning from personal experience: be sure and test your addition to the <code>fstab</code> configuration before rebooting because if you mess that up, it's possible you won't be able to start the OS. There are ways to mount and modify the configuration from a Windows computer but it's easier to just re-image the SD card (don't ask me how I know!).</p> <h3>Installing Hugo</h3> <p>The Hugo site <a href="https://gohugo.io/installation/linux/">recommends</a> using <a href="https://snapcraft.io/docs/installing-snap-on-raspbian">snap</a> but it restricts folder use to your user's folders. If all you're ever going to do is run it from your own user folders, then I suppose that's fine but I'm using an external drive mounted outside of my user area. Besides, installing Hugo on Ubuntu is a very simple step:</p> <p><code>sudo apt install hugo</code></p> <h3>Adding an SSH Key for Your Git Repository</h3> <p>If you use SSH to access your git repository (which is highly recommended) you need to set up an SSH key and add it to your repository. In order to prevent being prompted for an SSH key, you need to create a <a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys#deploy-keys">deploy key</a>.</p> <ul> <li>On the Raspberry Pi, run <code>ssh-keygen</code> (do <em>not</em> set a passphrase--it's only used on this one machine and needs to be automated; yes, there are ways to automatically load a passphrase and avoid the prompt but it's very tricky and not necessary as this is localized to this one repository, one machine, and it's a read-only key)</li> <li>You need to upload the generated public key to your GitHub account. The easiest way for me was to copy the key file (<code>.pub</code>) to a removable USB disk (or thumb-drive) that I temporarily mounted in the Raspberry Pi, then move the thumb-drive to my Windows machine where I could open the key file in a text editor.</li> <li>In your GitHub repository's settings' sidebar, under <em>Deploy keys</em>, click <em>Add deploy key</em>. Then copy the text from the key file and paste it into the new deploy key area with an appropriate title.</li> <li>Back on the Raspberry Pi, you need to load the private key when it boots so add the following lines to the <code>.bashrc</code>in your user's <code>$HOME</code> folder:</li> </ul> <pre> if [ -z "$SSH_AUTH_SOCK" ] ; then eval `ssh-agent -s` ssh-add fi </pre> <h3>Installing the GitHub Runner</h3> <p>Finally, it's time to download and configure the GitHub action runner. There are clear instructions that are dynamically customized for your repository on the GitHub website. Under your repository's <em>Settings</em> area, expand <em>Actions</em> in the sidebar, and click <em>Runners</em>. This will bring up a list of your currently installed runners--none, at first, of course. Click <em>New self-hosted runner</em> and you'll see a script with three platform buttons above it, <em>macOS</em>, <em>Linux</em>, and <em>Windows</em>, along with an <em>Architecture</em> drop-down. As you click each of these platform buttons and select the architecture corresponding to the computer where your runner will live, the command-line scripts to will change.</p> <p>The commands are rather long and since the Raspberry Pi doesn't have a GUI, you'll want to copy these to a text file, transport the text file to the Raspberry Pi on the thumb-drive in the same manner you brought the public SSH key file over. (Of course, if you're really adept at using <a href="https://vitux.com/four-web-browsers-for-the-linux-command-line/">a command-line oriented web browser on Linux</a>, you can skip this step.)</p> <p>One thing to remember when creating text files on Windows and porting them to Linux is that standard text files on Windows save both <code>CR</code> and <code>LF</code> for each line whereas Linux uses only <code>LF</code>. To execute these scripts directly on the Raspberry Pi, you'll want to make sure they're using the right text format when saving them on Windows. Most text editors have a simple option to change this.</p> <p>Once the action runner scripts are downloaded to your Raspberry Pi, run them to install, configure, and start your runner. Then refresh your web browser's view of the action runners associated with your GitHub repository and notice the new registered runner.</p> <h3>Creating a Workflow</h3> <p>Now that you have an Action Runner running, you need to tell GitHub two things:</p> <ul> <li>What triggers it?</li> <li>What to do when it launches?</li> </ul> <p>These are both defined in a <strong>Workflow</strong> file (written in <a href="https://phoenixnap.com/blog/what-is-yaml-with-examples">YAML</a>) that resides in the <code>.github/workflows</code> folder of your repository. You can have multiple workflow files in a repository and they can specify different action runners to launch and be triggered by different events. For my case, it was simple: one workflow that launches one runner.</p> <p>The workflow file starts off with a name to identify it, declares the GitHub event that will trigger it, then lists one or more jobs it will run, each with one or more steps. Here is the one for my personal photo website:</p> <pre> name: Update Personal Photo Website on: [push] jobs: rebuild-website: runs-on: self-hosted steps: - name: Refresh website files from repository run: cd ${{ vars.LOCAL_REPO }} &amp;&amp; git pull origin master --recurse-submodules shell: bash - name: Build HTML files for publishing run: cd ${{ vars.LOCAL_REPO }} &amp;&amp; hugo --minify shell: bash - name: Push to the web run: ${{ vars.LOCAL_REPO }}/upload.sh ${{ secrets.PHOTO_SITE_HOST }} ${{ secrets.PHOTO_SITE_USER }} ${{ secrets.PHOTO_SITE_PASS }} ${{ vars.LOCAL_SRC }} "/" shell: bash </pre> <p>The first line is the <code>name</code> of the workflow, "Update Personal Photo Website".</p> <p>The next line defines the trigger event, in this case <code>[push]</code> or when GitHub detects I've used the <code>git push</code> command to upload commits from my local repository. There are several other <a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows">events</a> you can choose from.</p> <p>Then comes the list of <code>jobs</code>. For my simple workflow, there's just one job: "rebuild-website" which specifies that it runs only on a self-hosted action runner and has three steps. Each step has a <code>name</code> and the details of how to accomplish that step. There are many things you can do in jobs and many pre-defined processes that can be launched in these steps; again, GitHub's documentation on <a href="https://docs.github.com/en/actions/using-jobs">Using Jobs</a> is pretty thorough. Since I'm controlling everything on my own machine, I wrote out the commands explicitly using the <code>run</code> keyword and specified the environment (or <code>shell</code>) in which it runs. Each run starts the shell afresh--when you change directory in one run command, it does not stay in that directory for the next command; that's why each of these three run statements either starts off with a <code>cd</code> command or references a file with the full path and filename.</p> <p>Note the last <code>run</code> command launches a script, <code>upload.sh</code>. This script runs an ftp client to upload files and it took a bit of testing to get it to work before I even added it to this workflow, so I just left it in a script and called it from this workflow. This shows the flexibility of using GitHub Actions--they can use pre-defined standard actions, or open-source scripts others have written, or low-level Linux commands--it all depends on your needs, your environment, and how much time you have!</p> <h3>Using Secrets and Variables</h3> <p>Each of the <code>run</code> commands reference <code>${{ secrets.___ }}</code> or <code>${{ vars.___ }}</code>. These get replaced when the workflow runs with values you've defined in a special section of the settings of your repository. Let's say you have a team of several developers and they use a local in-house website for their testing but they don't know the login credentials for the public FTP site--only the manager knows that. The manager can add special "secret" values that require extra authentication to change without exposing this critical information to a wider audience. Additionally, "variables" can be defined that are not hidden but may be used to customize a particular workflow that is used or copied for multiple purposes.</p> <h3>Testing Your Workflow</h3> <p>Now that your workflow is established and your self-hosted runner is registered and listening for triggered events, you are ready to test! There are two ways to do this, the first is obvious: you can perform the action that causes the workflow to be triggered, such as pushing a commit to the repository if, like mine, its trigger event is <code>[push]</code>. Additionally, you can go to your repository's settings sidebar, click on <em>Actions</em>, select the action, and launch it manually.</p> <p>While it runs, you can watch the progress of various jobs and steps on the GitHub page as the workflow progresses; it also counts up the amount of time that has elapsed. Here's a screenshot of a finished job run:</p> <img alt="GitHub Action run log" data-entity-type="file" data-entity-uuid="04ed67bc-a47a-4ea5-ac0c-67d7ea0d35f4" src="/sites/default/files/inline-images/rebuild-website-job.png" class="align-center" /> <p>I had to make several changes to my scripts, the workflow, and even a configuration change in the Hugo files until I got everything working consistently. Running Linux commands through an action runner is different than running them yourself from the command-line, so logging becomes an invaluable aid in trouble-shooting.</p> <p>There were two things in particular I needed to change because I was working on a Raspberry Pi rather than a desktop computer with more resources. First, the <code>timeout</code> setting in the Hugo configuration needed to be increased to several minutes from its default of 30 seconds because of the large amount of image files it was processing for my photo site on the slower CPU.</p> <p>Second, it ran out of memory.</p> <h3>Adding Virtual Memory</h3> <p>The Raspberry Pi 3 only has 1 GB of RAM. I am using very little of the 64-GB SD card used to boot and I don't care about the performance hit using a swap file will take because it's just a back-end process that runs occasionally when I upload a new photo album--I just don't want it to crash! Generally, the size of the swap should be at least equal to or twice the amount of RAM on your system, but it can also depend on your specific use case. For me, I added 4 GB, four times the amount of RAM (this may be overkill but it doesn't crash any more!)</p> <p>Here are the steps to create a swap file that adds virtual memory:</p> <ul> <li>Create a 4 GB swap file: <code>sudo dd if=/dev/zero of=/swapfile bs=4M count=1024</code></li> <li>Restrict read/write permissions to the root user: <code>sudo chmod 600 /swapfile</code></li> <li>Designate the file as a swap area: <code>sudo mkswap /swapfile</code></li> <li>Enable swapping on your system: <code>sudo swapon /swapfile </code></li> <li>Verify the swap file is now active: <code>sudo swapon --show</code></li> <li>Enable the swap file at start-up by adding <code>/swapfile none swap sw 0 0</code> to <code>/etc/fstab</code></li> </ul> <h3>Conclusion</h3> <p>My Raspberry Pi 3 is now a very useful part of my office, sitting on a shelf with nothing more than a power cord, an external hard disk, and a network cable. But it allows me to simply commit new photos to my repository--and my website is magically updated! Later, as I convert some of my existing static sites from Drupal to Hugo, there will be several more websites added in like manner.</p> </div> <section id="node-comment"> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=173&amp;2=comment_node_blog&amp;3=comment_node_blog" token="nfMl1M4GONrgzE9-QtHNcPxc8vb9AiQOuK0rcsaZ8OA"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Thu, 30 Mar 2023 06:06:39 +0000 david 173 at https://corneliusconcepts.tech https://corneliusconcepts.tech/automating-hugo-github-actions-raspberry-pi#comments Text Editors Revisited https://corneliusconcepts.tech/text-editors-revisited <span property="dc:title">Text Editors Revisited</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/miscellaneous" hreflang="en">Miscellaneous</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2023-03-12T19:43:55+00:00" datatype="xsd:dateTime">Sun, 03/12/2023 - 12:43</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>Admittedly, text editors isn't a terribly important topic to blog about but since I wrote a public comparison of <a href="https://corneliusconcepts.tech/ultraedit-vs-editpad-pro">UltraEdit vs EditPad</a> just over a year ago and since my preferences and stance on these have changed, I thought I should explain my new perspective. My license for the UltraEdit package was the All-Access Subscription, which means I had a full license to use all of the products in the suite for a year after which I would need to renew the license in order to keep using them. That renewal date arrived last month.</p> <p>I'll start by addressing each of the summarized items at the end of that blog which supported UltraEdit.</p> <ul> <li><strong>Support for HTML and Markdown editing</strong>: This is one feature that I really like with UltraEdit. Some editors have plug-ins to reformat HTML or Markdown. EditPad can recognize and syntax-highlight keywords (and actually has a lot of <a href="https://www.editpadpro.com/edithtml.html">support for editing HTML files</a>) but seeing the resulting HTML view in a side panel rendered immediately as you type is really convenient. For .md files, <a href="http://markdownpad.com/">MarkdownPad</a> is a great alternative for WYSIWYG editing of Markdown files.</li> <li><strong>Editable Macros and More</strong>: Extensive macro editing turned out not to be something I use. The <span><span><span>repetitive</span></span></span> tasks I need to do in a text editor are usually short; simply recording and playing back a set of keystroke is all I ever use--and which EditPad supports very well. For things more complex, I use Delphi.</li> <li><strong>Simplified Editor Tab Colors</strong>: You know what? I can change the editor tab colors in EditPad!</li> <li><strong>XML Formatting</strong>: EditPad has <a href="https://www.editpadpro.com/editxml.html">great support for XML</a> and can recognize and syntax-highlight XML nodes but doesn't nicely display the data in an editable XML structure. However, its <a href="https://www.editpadpro.com/manual.html#viewfilenavigator">File Navigation</a> panel shows the entire structure of an XML in a docked side panel and provides clickable links to the various nodes, giving you what you need for working with XML files.</li> <li><strong>Tag List and File View</strong>: While EditPad does not have the equivalent of UltraEdit's HTML Tag List, it does have <a href="https://www.editpadpro.com/acetext.html">snippets</a> which allow you store common snippets of text, code templates, HTML tags, etc. and insert them quickly into your text. For the File View feature, EditPad provides both an <a href="https://www.editpadpro.com/manual.html#viewfilebarexplorer">Explorer Panel</a> and <a href="https://www.editpadpro.com/manual.html#viewfilebarfiles">Files View</a>; so, that eliminates the advantage I thought UltraEdit had here.</li> </ul> <p>One of the big features that is often touted for using UltraEdit is its support of extremely large files. As I mentioned, EditPad works with large files just as easily. Also, Notepad++, a popular free text editor, can <a href="https://www.ghacks.net/2022/03/01/notepad-8-3-2-improves-very-large-file-handling-and-performance/">work with large files</a> from what I've read--I haven't tested it. So, I already possess an alternative editor that has no problem with big files--then again, I very seldom need to work with big files anyway.</p> <p><strong>Sidebars</strong>: One of the things I mentioned in the previous blog was all the sidebars that UltraEdit provides. When I think about it, EditPad provides most of this same functionality but while UltraEdit has different panels for various functionality (e.g. XML Manager ot Function List) EditPad groups those into a single, context-aware File Navigator panel. If I'm viewing an XML file, that panel shows XML nodes; if I'm viewing a Pascal unit, it shows parsed types, variables, functions, etc. in that panel and I can click on them to jump to that place in the file. This made me think that UltraEdit had more features but EditPad uses it's space more efficiently.</p> <p><strong>Other Editors - Notepad++</strong>: This is a popular open-source text editor and I've been using that recently at a job I started a few months back. It's a very capable text editor with a nice interface, plug-ins to provide additional functionality, and is well-supported. If you're already using this editor, there's likely no reason to switch.</p> <p><strong>Other Editors - VS Code</strong>: In my search to replace Drupal-based web frameworks I <a href="https://corneliusconcepts.tech/new-direction-web-development">blogged</a> about a few months ago, I looked at <a href="https://jekyllrb.com">Jeykyll</a> and <a href="https://gohugo.io">Hugo</a> among others. Every single one of the tutorials or demos I watched to see how to build a site with a particular tool, used <a href="https://code.visualstudio.com/">VS Code</a> (and most of the demos were done on Macs)! I was surprised how popular this text editor has grown and matured. It is free, runs on EVERY platform, and has a huge following with many plug-ins. One of my complaints a year ago was that every time I opened it, there would be an update--but I only opened it a few times a month and with several plug-ins installed, the chances were pretty high one of them would need an update.</p> <p>As I researched various open-source web tools and installing and configuring things while watching YouTube videos, I installed VS Code again and started using it--on both Windows and Linux. I found that it acts and functions exactly the same on the two platforms and when I'm using it every day, the frequency of updates seems very minimal. It's great for working with several different programming languages, file formats, and web frameworks. I won't do an in-depth comparison and have not tried loading large files again but suffice it to say that this is a great tool for working on various technical projects.</p> <p><strong>Other Editors - Atom</strong>: Because of the great surge in popularity of VS Code, <a href="https://en.wikipedia.org/wiki/Atom_(text_editor)">Atom has been deprecated</a>.</p> <h3>Conclusion</h3> <p>When the renewal came up, I looked at how I had used the tools over the past year, my new use of VS Code, re-discovered features of EditPad and the fact I didn't have to pay anything for these two tools. Another minor thing in the back of my head was the user interface. EditPad's is well-designed and easy to read with intelligent use of icons, colors, and text size--and like I mentioned above, context-aware aware panels for minimal clutter. I found some parts of UltraEdit to be confusing, or too small to read; some parts were fine but it wasn't consistent, even when switching color schemes. I found myself hunting through various menus for things (was this panel in the Layout or Windows or View menu? Or was it a checkbox option somewhere?). Perhaps that speaks to EditPad's design--or it's just my familiarity with the product after several years of use.</p> <p>I did give <a href="https://www.ultraedit.com/products/ultraedit/">UltraEdit</a>--and it's companion tools, <a href="https://www.ultraedit.com/products/ultraftp/">UltraFTP</a>, <a href="https://www.ultraedit.com/products/ultracompare/">UltraCompare</a>, and <a href="https://www.ultraedit.com/products/ultrafinder/">UltraFinder</a>--a fair try, but didn't find them to be so superior that I should pay to keep using them over what I already have. <a href="https://winscp.net">WinSCP</a> beats UltraFTP, <a href="https://sourcetreeapp.com/">SourceTree</a>'s diff compare is usually sufficient for file comparison but I also have <a href="https://www.scootersoftware.com/">BeyondCompare</a>, and the command-line tool <a href="https://www.windows-commandline.com/findstr-command-examples-regular/">FindStr</a> (or <a href="https://www.howtogeek.com/496056/how-to-use-the-grep-command-on-linux/">grep</a> on Linux) usually satisfies any file searching needs I have. So between <strong>EditPad </strong>on my Windows machines and <strong>VS Code</strong> for cross-platform web development, I'm good!</p> <p><em>Well, for now anyway...</em></p> </div> <section id="node-comment"> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=172&amp;2=comment_node_blog&amp;3=comment_node_blog" token="3vIc642txP9BUbX6ImLuHQEaBixpZsmE8R75-9b3N4c"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Sun, 12 Mar 2023 19:43:55 +0000 david 172 at https://corneliusconcepts.tech https://corneliusconcepts.tech/text-editors-revisited#comments Go! https://corneliusconcepts.tech/go <span property="dc:title">Go!</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2023-01-31T20:01:40+00:00" datatype="xsd:dateTime">Tue, 01/31/2023 - 12:01</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>As mentioned in my <a href="https://corneliusconcepts.tech/new-direction-web-development">last entry</a>, I've recently become curious about the <a href="https://go.dev">Go programming language</a>, so I took a <a href="https://www.udemy.com/course/learn-go-the-complete-bootcamp-course-golang/">course</a> to learn the language, partially to support add-ons for <a href="https://gohugo.io">Hugo</a> and partially to compare with other languages I've used--most notably, Delphi. What I found was a simple but powerful language, thoughtfully designed, that promotes good coding style and breaks some established patterns of thought I've had with object-oriented programming.</p> <p>One of the big advantages of Go over other popular languages is that, like C++ and Delphi, it compiles to an executable binary instead of being scripted like PHP, Python, or Ruby. This is a proudly touted feature that also helps promote Hugo.</p> <p>The syntax, at first glance, appears to mimic C or JavaScript but there are a couple of surprising similarities with Delphi. Furthermore, assumptions made by the compiler that encourage you to simplify your code, make it resemble Python in a way.</p> <p>In this blog, I'll give a brief overview of what I learned about the Go language from the perspective of a programmer who's used mostly Delphi (Pascal) but with a sprinkling of other languages (C, C++, C#, VB, and PHP) throughout his career.</p> <p>First, a disclaimer: I'm very new to the Go programming language and some of what I describe may not be completely accurate--it's based on what I understood as I went through the introductory course I just completed and the simple programs I wrote. There are likely exceptions to what I describe or the generalities expressed may not be as prevalent in the industry as I suspect. Also, my concentration was simply the language and writing simple console apps--I have not yet been exposed to the wide array of packages in the Go ecosystem or done any Windows or cross-platform exploration with the language.</p> <h3>Packages</h3> <p>A file of Go code (a <code>module</code>) starts with the keyword <code>package</code> and a name. All <code>.go</code> files in the same directory typically share the same package name. To use functions and types from an external package of library code, you <code>import</code> it (like the <code>uses</code> clause in Delphi). The <code>.go</code> files in the same folder do not have to be explicitly imported--they're automatically included in the package.</p> <p>Go is a case-sensitive language. This is especially important when naming functions and identifiers at the package level. In Delphi, things that need to be accessed outside of a unit are placed in the <code>interface</code> section and the hidden details are in the <code>implementation</code> section. In Go, you simply capitalize identifiers (proper-cased, not all caps) to make them visible outside of the package; because of this, package-level identifiers are lower-case (or at least start with a lower-case letter) unless you need them available from elsewhere.</p> <h3>Functions</h3> <p>Functions start with the keyword <code>func</code> followed by the function name and parenthesis with optional parameters inside the parenthesis followed by an optional return type and then the block of code that makes up the function. Blocks are delineated in Delphi with <code>begin</code> and <code>end</code> but use curly braces in Go--just like C# and Java and several other languages. When a Go program starts, it always looks for and runs the <code>main</code> function:</p> <pre> func main() { }</pre> <p>If the function does not return a value, it's return type is simply left off as in the <code>main</code> function depicted above; this is like a <code>void</code> function in C and the <code>procedure</code> type in Delphi. Unlike other any other language that I know, Go functions can have multiple return values. A typical use for this is to return an error along with the result; for example, the <code>Atoi()</code> function from the <code>strconv</code> standard Go package, converts a string to an integer, returning both the integer (if successful) and the possible error:</p> <pre> n, err := strconv.Atoi(s)</pre> <p>You can then test <code>err</code> and if it's <code>nil</code>, proceed to use the successfully converted integer, <code>n</code>. In my opinion, this is much nicer than wrapping this in a try-except block in Delphi or returning the error in a <code>var</code> parameter. Speaking of <strong>parameters</strong>, there are no <code>var</code> parameters in Go; all parameters in Go are passed by reference; however, you can also pass pointers and the functions can modify the value at which they point.</p> <p>A function with two parameters and one return value could be declared like this:</p> <pre> func DoSomething(n int, s string) bool { // } </pre> <p>To declare two return values, wrap them in parenthesis:</p> <pre> func ConvertValue(v string) (n int, ok error) { // } </pre> <p>By the way, <code>error</code> is a built-in "interface" type and has an <code>Error() string</code> function. I'll talk about interfaces below.</p> <h3>Semicolons</h3> <p>You may have noticed the sample Go code here does not have any semicolons at the ends of the lines. While the formal Go grammer does require semicolons to terminate statements, an interesting aspect of the Go lexer is that it <a href="https://go.dev/ref/spec#Semicolons">automatically inserts them as it scans the code</a> based on the context of the line. You can have them in your code but they're not necessary in most cases--and therefore, discouraged.</p> <h3>Variables</h3> <p>If you saw the variable assignment above with the <code>:=</code> and caught your breath exclaiming, "That's like Delphi!" then I'll pause ... and let you revel in this short moment because the <code>:=</code> has a very special use case--and it's not used as often as you might hope.</p> <p>Go, like Delphi, is a type-strict language; each variable has a specific type established when it is declared. There are two ways to declare variables. The first is somewhat similar to Delphi because it uses the <code>var</code> keyword (but doesn't use a colon between identifiers and their type); there isn't a <code>var</code> declaration section, each variable is defined on its own line whenever you need one:</p> <pre> var n int var s string </pre> <p>Later, you can assign values to them with a single <code>=</code> sign:</p> <pre> n = 1 s = "hello" </pre> <p>The second way to declare a variable is by initializing it with a value from which it gleans the type and declares it implicitly. This is the special case (and the only time) where the <code>:=</code> is used in Go:</p> <pre> n := 1 s := "hello" </pre> <p>In this case, there's no need for the <code>var</code> keyword or explicitly declaring the variable types as the type is inferred with the value assignment; in other words, <code>n</code> is implicitly declared as an <code>int</code> and <code>s</code> as a <code>string</code>. The <code>:=</code> can only be used once on an identifier within a scope; after declaring and initializing a variable, further assignments can only be done with the single <code>=</code> operator.</p> <p>One other note is that declaring a variable without an immediate assignment of your value always initializes the variable to the "<a href="https://go.dev/ref/spec#The_zero_value">zero-value</a>" for its type; numeric variables are set to zero, string variables to an empty string, pointers to nil, and Booleans to false; this is also true with arrays where each element of the array is initialized to the zero-value for its element type. This is nice to count on rather than be subject to the fate of whatever value happens to be in the memory location for a new variable--I've seen many hard-to-find bugs because of uninitialized variables (which, of course, a good programmer would never have!).</p> <h3>Types</h3> <p>Speaking of variable types, here are the basic variable types in Go:</p> <ul> <li><code>bool</code> - (true/false)</li> <li><code>byte</code> - (alias for uint8)</li> <li><code>rune</code> - (alias for int32)</li> <li><code>int</code> - (many variations--see below)</li> <li><code>float32/float64</code></li> <li><code>complex64/complex128</code></li> <li><code>string</code></li> <li><code>struct</code></li> <li><code>interface</code></li> </ul> <p>Like Delphi, there are many variations of <strong>integers </strong>depending on the size of number you need to deal with and the platform you're on: int8 (-128..127), uint8 (0..255), int16, uint16, int32, uint32, int64, and uint64; the standard <code>int</code> type is either int32 or int64 depending on the platform.</p> <p><strong>Strings </strong>are immutable and 0-indexed. For example, in <code>s := "Hello"</code>, the first character is referenced with <code>s[0]</code> and the last one with <code>s[len(s)-1]</code>.</p> <p>Each of the basic types can be used in a <strong>arrays</strong>, <strong>slices</strong>, or <strong>maps</strong>. You've likely heard of arrays but what are slices or maps? A <code>slice</code> is simply a portion of an <code>array</code> and, in fact, always has a "backing array" to which it points, either created implicitly (e.g. with the built-in <code>append()</code> function) or assigned to specific elements of an array explicitly. Arrays always have a fixed length, slices can grow or shrink. A <code>map</code> is like the <code>TDictionary</code> type in Delphi and can be used to create an index of any type to a value of any other type. Here are some examples of these types:</p> <pre> // declare an array of 10 numbers var nums [10]int // declare a slice of integers; then append the first five elements (0 to 6-1) from the nums array var nums5 []int nums5 = nums[0:6] // declare a map of three-letter acronyms, initialized with 3 entries dict := map[string]string { "TLA": "Three-Letter Acronym", "API": "Application Programming Interface", "AFK": "Away From Keyboard", } </pre> <p>Slices are more powerful and flexible than you might at first think--and the <a href="https://go.dev/doc/effective_go#slices">topic</a> gets deep quickly.</p> <p>A <code><strong>struct</strong></code> is like the <code>record</code> type in Delphi: it is a user-defined type containing fields. However, structs cannot contain functions; instead, they are associated with a struct type in the function's declaration like this:</p> <pre> type person struct { fname string lname string age int } func (p person) fullname() string { return p.fname + " " + p.lname } </pre> <p>The "fullname" function is associated with the "person" type and contains an implicit parameter of type "person" that can be used within the function. When considering Delphi's class or record types, this seems rather loosely connected--but it works and it's simple. (Simplicity is an overarching goal of the Go language.)</p> <p>An <code><strong>interface</strong></code> in Go is very similar to Delphi's <code>interface</code> type in that it is an abstract type that declares a list of function names that must be implemented but has no implementation itself.</p> <p>For example, the following establishes a "printer" type listing one function, <code>print()</code>, which must be implemented and associated with a struct for the struct to be considered as adhering to the set of "printer" types:</p> <pre> type printer interface { print() }</pre> <p>Let's say we have a store with books and games. We could create a couple of different types and as long as each implements <code>print()</code>, then they will both implicitly match the <code>printer</code> interface:</p> <pre> type book struct { title string author string price float } func (b book) print() { fmt.Printf("%s, by %s - %.2f\n", b.title, b.author, b.price) } type game struct { title string category string price float } func (g game) print() { fmt.Printf("%s [%s] - %.2f\n", g.title, g.category, g.price) } </pre> <p>Now, if you had an array of <code>printer</code> interfaces loaded with various <code>book</code> and <code>game</code> items, you could call the <code>print()</code> function and it would call the appropriate one for the element's type:</p> <pre> var inventory [100]printer // ... inventory[i].print() </pre> <p>I found it interesting that you don't actually declare the <code>book</code> or <code>game</code> types as being of the <code>printer</code> interface, just the fact the associated function <code>print()</code> exists is all it takes to qualify. It's design provides for simple dependency injection.</p> <p>A couple of important aspects of the <code>struct</code> type I should mention before moving on is that structs cannot inherit from other structs but <code>struct</code> fields can be of any type, including another defined <code>struct</code> type; a struct with a field of another struct is called "embedding" and there are some shortcuts for accessing fields that make working with them simple--as you would expect with Go.</p> <p>Here's an example of a <code>struct</code> type with an embedded struct field referencing the "person" struct type I showed earlier:</p> <pre> type employee struct { p person title string salary float }</pre> <p>With this, you can reference the <code>fname</code> and <code>lname</code> fields (defined by the <code>p person</code> field) as if they were native to the <code>employee</code> struct.</p> <p>There is no "class" type in Go; the <code>struct</code> with embedded sub-types and associated functions and its use of an <code>interface</code> to identify related functions as depicted above are what Go considers all you need to support object-oriented programming. But Go doesn't really try to classify itself as a standard OOP language; indeed, it's <a href="https://go.dev/doc/faq#Is_Go_an_object-oriented_language">FAQ</a> says this, "<em>Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general.</em>" While going through the Go course, I kept looking for stronger implementations of how I learned object-oriented programming but as I read articles about why the language was designed like it is, I started to think that maybe all the OOP syntax I've come to expect and work with is more of a restriction than protection. Like I said earlier, it works and it's simple.</p> <p>Notice the lack of a "date" or "time" type? A <code>time</code> struct is defined in the standard "<a href="https://pkg.go.dev/time">time</a>" package--it's not a native type.</p> <h3>Flow Control</h3> <p>The <code><strong>if</strong></code> control structure is nearly identical to the C/C++/C# way of doing things. Its initialization clause can declare and initialize a variable:</p> <pre> if x := f(); x &lt; y { return x } else if x &gt; z { return z } else { return y }</pre> <p>The <code><strong>switch</strong></code> statement is also similar to the C class of languages, but with a couple of simplifying enhancements. The Boolean expression part of the syntax can be preceded by an assignment:</p> <pre> switch s := getinput(); s { case "one": return 1 case "two": return 2 default: return 0 }</pre> <p>The expression can also be left out completely which means the <code>case</code> statements need full Boolean expressions of their own:</p> <pre> switch { case x &lt; y: return "less" case x &gt; y: return "more" case x == y: return "equal" }</pre> <p>Switch statements in Go can also test a variable's type:</p> <pre> switch x.(type) { case int32, int64: return "integer" case bool: return "boolean" case string: return "string" } </pre> <p>Notice no <code>break</code> statements? In Go, each <code>case</code> has an implicit "break" at the end before the next <code>case</code>. However, you can override that with the <code>fallthrough</code> statement which resumes control with the next <code>case</code>:</p> <pre> switch num := number(); { case num &lt; 50: fmt.Printf("%d is less than 50\n", num) fallthrough case num &lt; 100: fmt.Printf("%d is less than 100\n", num) fallthrough case num &lt; 200: fmt.Printf("%d is less than 200", num) default: fmt.Printf("%d is out of range", num) }</pre> <p>Personally, I find this a much smarter approach than C's <code>break</code> as it's far more common to handle each case separately; <code>fallthrough</code> is most assuredly the rarer case, thus Go's switch syntax is cleaner.</p> <p>The only <strong>looping </strong>construct in Go is the <a href="https://go.dev/doc/effective_go#for">for</a> statement--there is no repeat-until, while-do, or do-while; the flexibility of Go's <code>for</code> allows you to do it all with this one statement and its variations.</p> <p>We'll start with the standard C-like syntax that initializes a variable and repeats the block of code until a condition is false and incrementing at the end of each loop:</p> <pre> for i := 1; i &lt; 10; i++ { // }</pre> <p>To implement a "while" loop, provide only a conditional:</p> <pre> for getDigit() != 0 { // }</pre> <p>You can even write an infinite loop, assuming somewhere in the block is a <code>break</code> or <code>return</code> statement:</p> <pre> for { // break }</pre> <h3>A Complete Program</h3> <p>Those are the basics. Before I close, I'll share a complete Go console program, explanation will follow:</p> <pre> package main import ( "bufio" "fmt" "os" ) func main() { if len(os.Args) != 2 { fmt.Println("Usage: go run readfile.go &lt;filename&gt;") os.Exit(1) } filename := os.Args[1] file, err := os.Open(filename) if err != nil { fmt.Println("Error opening file:", err) os.Exit(1) } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { fmt.Println(scanner.Text()) } if err := scanner.Err(); err != nil { fmt.Println("Error reading file:", err) os.Exit(1) } } </pre> <p>In this example, the program first checks if the number of command-line arguments is exactly 2 (the program name and the filename). If not, it prints an error message and exits with a status code of 1.</p> <p>Then, it opens the file specified by the filename passed on the command-line, reads the lines in the file and prints each line; if there's an error reading the file, it prints an error message and exits with a status code of 1.</p> <p>Finally, the file is closed using <code>defer</code> so that it will be closed even if there's an error or panic in the program.</p> <h3>Summary</h3> <p>There are many other interesting tidbits about the language, such as "named" versus "positional" parameters, the <a href="https://go.dev/doc/effective_go#defer">defer</a> keyword, using <code>range</code> to iterate over a collection of items such as an array or slice, and how to recover from a call to <a href="https://go.dev/doc/effective_go#panic">panic</a>. My introduction to the language was a fun learning experience. I'm impressed with this language but not sure yet how much I'll do with it as Delphi is my bread-and-butter. As stated earlier, I do intend to use it in conjunction with migrating Drupal sites to Hugo using open-source migration tools written in Go but I still don't know how to access databases or build a GUI application. I have a ways to go and not as much spare time as I'd like.</p> </div> <section id="node-comment"> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=171&amp;2=comment_node_blog&amp;3=comment_node_blog" token="UEbjXG3SjQBjSVUbjMa-cM7X5A5He6yA2y5Wcl4yW18"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Tue, 31 Jan 2023 20:01:40 +0000 david 171 at https://corneliusconcepts.tech https://corneliusconcepts.tech/go#comments A New Direction for Web Development https://corneliusconcepts.tech/new-direction-web-development <span property="dc:title">A New Direction for Web Development</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/cloud" hreflang="en">Cloud Computing</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2023-01-01T03:02:41+00:00" datatype="xsd:dateTime">Sat, 12/31/2022 - 19:02</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>Back in May, I lamented about my <a href="/major-drupal-upgrade-woes" rel="nofollow">struggles to keep my Drupal websites updated</a>. If I had command-line access to the web server, I could use its recommended method, <a href="https://getcomposer.org" rel="nofollow">Composer</a>, and a cron job could magically keep everything updated for me--but I use inexpensive shared web hosting and that is not an option. This means I must manually upload a bunch of PHP files for each Drupal update. Version 7 wasn't that bad at only 12 MB and just over a 1,000 files but Drupal 8's footprint jumped up to 73 MB and over 18,000 files--and it increased further with Drupal 9 (I haven't checked the size of Drupal 10 which was released this month). The worst part is that all the files are date-stamped with the date the version came out so I can't simply upload only the new and changed files--they ALL look new! I'd just deal with it if it was an update to one or two sites every other month, but I maintain eight separate sites and there are often multiple security updates each month! Yes, I've reached my breaking point with maintaining Drupal sites on shared web hosting--it's time for something simpler, especially since most of my sites have infrequent changes and don't really need the power of a dynamic scripting language tied to an online database (it seems like I went from simple HTML in the 1990s to dynamic sites over the last 20 years and now am wanting to go back to simple HTML--what's old is new again!).</p> <p>A comment on last May's blog opened my eyes to a different approach: use a static website generator. Since I'm a big fan of Delphi and know it's fully capable of generating web sites in a variety of ways, my first thought was to write my own. But a little bit of research revealed there are dozens (hundreds even?) of website generators, in nearly every programming language, and most are open source--so why reinvent the wheel? It would take far less time to study and put to use the efforts of those who have gone down this path already than it would for me to design and write one myself. Besides not EVERYTHING has to be done in Delphi!</p> <p>As I read reviews and tried a few of them out, I kept coming back to the commenter's suggestion: <a href="https://gohugo.io" rel="nofollow">Hugo</a>. Whatever tool I was going to use had be well-supported, have lots of pre-built themes, good community support and documentation, and (hopefully) be able to import the content from my existing Drupal sites--Hugo is one of the few where I found direct support for <a href="https://github.com/search?q=hugo+drupal" rel="nofollow">converting Drupal sites</a>. The more I looked at Hugo, the more I realized I would likely need to know the programming language Hugo is written in: <strong>Go</strong>.</p> <p>So, I paused my research on website generators and took a deep look at <a href="https://golang.google.cn" rel="nofollow">Go</a>, or Golang as some refer to it. Many of today's modern languages, especially ones used in conjunction with building or running websites, are scripted: PHP, JavaScript, and Ruby just to mention three popular ones.  Go, however, is compiled--that was one surprising and positive aspect that that struck me. It's also a relatively new language (less than 15 years old at this point), designed by the engineers at Google, has security and concurrency built in, is available on nearly every platform, forces clean and simple coding, is <a href="https://github.com/gohugoio/hugo" rel="nofollow">open source</a>, and is well-supported with a growing user base and well-written documentation.</p> <p>My goal for the new year is to learn this new programming language and jump whole-heartedly into working with Hugo to convert several of the Drupal sites I maintain. Uploading one or two changed HTML files when there's a content update and not worrying about constantly updating Drupal or fighting with PHP version incompatibilities will take significantly less time and worry! That sounds like a positive direction for 2023!</p></div> <section id="node-comment"> <article data-comment-user-id="0" id="comment-529" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1672758654"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/529#comment-529" class="permalink" rel="bookmark" hreflang="en">Try a different web hosting provider?</a></h3> <div class="single-comment-meta"> <span><span lang="" typeof="schema:Person" property="schema:name" datatype="">John (not verified)</span> Tue, 01/03/2023 - 04:48</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>Hi David,</p> <p>What hosting provider are you using? I use interserver and I have direct access to the terminal and can run commands. Plus, they are very affordable and with great customer support.</p> <p>Things have changed dramatically in shared hosting. I would also expect the cpanel (or equivalent) in your current hosting to recognise the Drupal installations and offer to update them automatically when a new version arrives. </p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=529&amp;1=default&amp;2=en&amp;3=" token="J46rgvlw9Sftu1LA4PGxSN5uqHWyKrIh2O54P2oPYLM"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="indented"> <article data-comment-user-id="1" id="comment-531" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/1"> </article> <mark class="hidden" data-comment-timestamp="1672759730"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/531#comment-531" class="permalink" rel="bookmark" hreflang="en">It&#039;s an excuse to learn a new language!</a></h3> <div class="single-comment-meta"> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span> Tue, 01/03/2023 - 07:28</span> <p class="visually-hidden">In reply to <a href="/comment/529#comment-529" class="permalink" rel="bookmark" hreflang="en">Try a different web hosting provider?</a> by <span lang="" typeof="schema:Person" property="schema:name" datatype="">John (not verified)</span></p> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>My sites are on <a href="https://ionblade.com/" rel="nofollow">ionBlade</a> which has an option to install Drupal on a new site but I've installed it manually by uploading files and running the install.php script. Perhaps I should've used their installer but there's a lot about that process I don't know and wanted more direct control. If I upgraded to a more expensive tier, I'm sure shell access would be available</p> <p>I used InterServer many years ago. I forget why I left but I'm in the middle of a 3-year contract with ionBlade and except for this one restriction (which I've also experienced with another web host), I'm happy with them.</p> <p>There are other aspects of moving to Hugo I'm looking forward to: simpler content management of static sites, being free of server software dependencies, and an excuse to learn a new programming language. ;-)</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=531&amp;1=default&amp;2=en&amp;3=" token="xVTNhN6jtROQLif3UvFflzMqCBSYmO6yDft04hE9OWU"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> </div> <article data-comment-user-id="0" id="comment-532" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1673590530"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/532#comment-532" class="permalink" rel="bookmark" hreflang="en">RemObjects Gold</a></h3> <div class="single-comment-meta"> <span><a rel="nofollow" href="https://www.emoconsult.nl" lang="" typeof="schema:Person" property="schema:name" datatype="">Erwin Mouthaan (not verified)</a> Thu, 01/12/2023 - 05:26</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>"Besides not EVERYTHING has to be done in Delphi!"</p> <p>Almost everything. With RemObjects Gold you can mix Go with Oxygene within same project. Even third-party Go libraries can be used with Oxygene language. You still have an excuse.</p> <p>https://www.remobjects.com/elements/gold/</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=532&amp;1=default&amp;2=en&amp;3=" token="3zQ04VPk78JDx5wW1mEZzzJCERgegdVQakZWUtTh2fA"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=170&amp;2=comment_node_blog&amp;3=comment_node_blog" token="rMtaw-_orhY7pW_upeQvQH7HWVMHR9h-OMKWZg6LezQ"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Sun, 01 Jan 2023 03:02:41 +0000 david 170 at https://corneliusconcepts.tech https://corneliusconcepts.tech/new-direction-web-development#comments For Historical Purposes https://corneliusconcepts.tech/historical-purposes <span property="dc:title">For Historical Purposes</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2022-11-18T19:21:38+00:00" datatype="xsd:dateTime">Fri, 11/18/2022 - 11:21</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>Early in my career, I was studying the code of an application written for the Apple II in preparation for developing something similar on the PC and would often ask questions of the original programmer. Most of the time, I'd get valuable information about the purpose of a routine or why something was done a particular way. But every once in a while, when pressed for an explanation, the programmer would think for a minute, then simply utter, "For Historical Purposes" and walk away chuckling.</p> <p>I was somewhat frustrated because I wanted to know the "WHY" of everything--there had to be a good reason for coding things a certain way, right?</p> <p>Well, it turns out, I've had to use that very phrase many times since those early days. Sometimes there are good reasons why code turns out the way it does--sometimes not so good. It could be that new requirements are coming so fast, you don't have time to rewrite a routine to fit multiple scenarios so you have to duplicate the effort to fit each instance. Working on legacy code where there have been several developers touching various parts over the years, there are bound to be questions about the code that no one can answer--sometimes because not all programmers play well together and have very different ideas on how things should be done, other times, a new developer may not know about how something was solved previously and do it again--differently.</p> <p>I was recently working on a legacy Delphi 5 project and needed to implement some checks in a data entry screen. There were several parts of the application that used this type of data and so the same check needed to be implemented in multiple places.  Once I figured out how to use existing data fields in the first screen, I figured it would be a simple copy-paste to the other modules. But no, in the second screen I worked on, instead of a dataset ready to use, a function went out and grabbed all the necessary information and stored it in a class. That's a better approach but now instead of simply accessing some readily available data fields, I need to go modify the class, then modify the method that fills the class before I can use it.</p> <p>The important thing is to be flexible and observant and figure out how things ought to be done but also keep a keen eye for reusing whatever constructs are already available to prevent re-inventing the wheel. Sometimes that involves searching with the right keywords, sometimes that asking the right person.</p> <p>And if the answer is vague and involves some hand-waving with references to a distant pass, just smile and get back to work!</p></div> <section id="node-comment"> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=169&amp;2=comment_node_blog&amp;3=comment_node_blog" token="Oy1A4IKmSmhHDx5Rx0k3EvtRYFx6hc99PqbzoE_ZVlE"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Fri, 18 Nov 2022 19:21:38 +0000 david 169 at https://corneliusconcepts.tech https://corneliusconcepts.tech/historical-purposes#comments Thankful for Delphi https://corneliusconcepts.tech/thankful-delphi <span property="dc:title">Thankful for Delphi</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/programming" hreflang="en">Programming</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2022-11-16T16:15:01+00:00" datatype="xsd:dateTime">Wed, 11/16/2022 - 08:15</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>This time of year in the United States is marked by a major holiday, Thanksgiving, the last Thursday of November. It is during this time that ad campaigns, religious organizations, and families everywhere tend to step up their recognition of everything they're thankful for. As a software developer, I'd like to hook into this theme and highlight features of programming tools I use that make my life better--most notably, Delphi--and create my own "thankfulness" list.</p> <p>Part of the drive behind this is the fact I've been using Delphi 5 and the BDE a lot lately to support some legacy code. Fortunately, there are plans to majorly upgrade the code base and database connectivity to newer technologies but it has definitely reminded me of how much better the newer versions are--and how much everything has progressed in the last 22 years.</p> <p>But first, while I'm using Delphi 5, I'll point out a few things for which I am thankful:</p> <ul> <li><strong>Backwards compatibility</strong>. I guess you could say this is both a blessing and a curse: a blessing because this old compiler still produces Windows applications that run on the most recent versions of Windows but also that very fact means there hasn't been a hard reason to force an upgrade of the code base.</li> <li><strong>GExperts</strong>. One of the most popular of all Delphi IDE plugins, <a href="https://www.gexperts.org" rel="nofollow">GExperts</a> is an invaluable aid as it provides so many wonderful productivity benefits, I can't imagine programming in Delphi (especially Delphi 5) without it. With its Grep Search, Set Tab Order feature, Favorite Files list, Replace Components capability, Code Librarian, Multi-line Component Palette, and (my favorite) Code Proofreader, there are many valuable and time-saving programming aids--too numerous to mention here.</li> <li><strong>Castalia</strong>. Another great IDE plug-in, this one provides, among other things, stack-based bookmarks and <a href="https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Code_Editor#Structural_Highlighting" rel="nofollow">structural highlighting</a> within your code. This was originally a commercial product by TwoDesk Software but was <a href="https://www.embarcadero.com/press-releases/embarcadero-acquires-castalia-and-usertility-from-twodesk-software" rel="nofollow">acquired by Embarcadero</a> in 2015 and subsequently integrated into the Delphi IDE over the next few versions.</li> <li><strong>Optional Parameters</strong>: This is a language feature that comes in handy so many times, like when you need to make a small change in a method for a particular case but you don't want to change the functionality for the many other cases for which it was originally written.  By adding an optional parameters and defaulting it to handle all the other cases, you can keep current functionality, don't have change gobs of code, and still handle your new situation.</li> <li><strong>Speed</strong>. Because there weren't as many features in Delphi 5 as there are in newer versions of the IDE, it loads really fast on today's modern computers--another ones of those pro-versus-con type of things.</li> </ul> <p>Speaking of newer versions of Delphi, I'm glad the product has continued to evolve and change. There are sometimes lags in features I've seen in other development tools but by and large, Delphi is quite feature-rich and highly productive. Here are the things I'm most thankful for (in no particular order):</p> <ul> <li><strong>Even more plug-ins</strong>: In addition to the aforementioned GExperts (still available in Delphi 11) and Castalia (features merged into the IDE) there are additional plug-ins. I've written about a couple of these before: <a href="https://corneliusconcepts.tech/my-favorite-new-delphi-103-rio-features" rel="nofollow">Bookmarks and Navigator</a>, originally by Parnassus. These provide additional bookmarking features and add a mini map inside the code editor.  When Delphi XE8 introduced a new Welcome Page, I <a href="https://corneliusconcepts.tech/make-new-welcome-screen-delphi-usable" rel="nofollow">complained but found a way to improve it</a>; then I was very grateful when <a href="https://www.danielwolf.eu/blog/2015/1668-meine-vorstellung-einer-willkommens-seite" rel="nofollow">Daniel Wolf wrote a replacement</a> which helped me manage the plethora of projects I was working on. Now, in Delphi 11, another interface has arisen but it's much more flexible and has an API--which has already been used for another <a href="https://dwp.gksoft.ch" rel="nofollow">multi-project management plug-in</a>.</li> <li><strong>Third-Party Components</strong>: There are many software vendors that support the Delphi language with libraries and components. I've used several of them over the years to add highly-functional grids, spell-checking, enable a plug-in system, and provide a richer experience for my users.</li> <li><strong>Backwards Compatibility</strong>: I mentioned this before but meant something different. Not only can old programs compiled with Delphi 5 run on new Windows operating systems, but old Delphi code can be upgraded with little effort (many times) to compile in newer versions of Delphi. There might be many things that need to be adjusted to take advantage of newer technologies (e.g. migrating from the BDE to FireDAC) but at least you can often get old code to compile without too much heartache--seldom do you need to completely rewrite something unless third-party components are no longer supported.</li> <li><strong>Attributes</strong>: At first, I didn't really understand the need for attributes but after I saw a few examples of where you could add functionality to a class without changing the class's purpose or making multiple derivations of the class, it really started making sense.</li> <li><strong>Generics</strong>: Another language feature added several years ago, this makes building complicated object-oriented applications much nicer in that you don't have to cast to-and-from a standard TObject.</li> <li><strong>REST Support</strong>: It's so nice to tap into a huge variety of public APIs with native libraries that come with Delphi. And by using the <a href="https://corneliusconcepts.tech/trestresponsedatasetadapter-and-hidden-gems-rest-debugger" rel="nofollow">REST Debugger and it's Copy Components feature</a>, you can create these applications very quickly.</li> <li><strong>Live Templates</strong>: There are many time-saving features in code editors but I think one of my favorites (after color-syntax highlighting, code-formatting, auto-indenting, and so forth that we come to expect in all editors these days) is the ability to type abbreviations and have them immediately expand to words or even context-aware code fragments that understand how to complete a class, add variable declaration, wrap a try-finally around an object, and so forth. I talked about my <a href="https://corneliusconcepts.tech/delphi-productivity-tips-live-templates" rel="nofollow">love of Live Templates</a> over a year ago.</li> <li><strong>Error Insight</strong>: I really like having the compiler check my code as I type and warn me of mistyped variable names or invalid method parameters; it saves time later when I compile and then find out my mistake. Read more about this and the related <a href="https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Code_Insight_Reference#Error_Insight" rel="nofollow">Code Insight features on Embarcadero's Docwiki</a>.</li> <li><strong>Flexible IDE</strong>: I like the fact that the IDE's windows are dockable and resizable and that you can save layouts. Every programmer is different and even I use Delphi a little differently when I'm on a laptop than when I'm on my desktop computer with multiple monitors. In fact, I've even had specific layouts for certain projects!  And I <em>really</em> like the fact that you can have a Debug layout that it switches to automatically when debugging an application.</li> <li><strong>Speed</strong>: The Delphi compiler is still one of the fastest on the market. I like being able to hit F9 and see only the modified units compiled, linked, and the program start up so quickly.</li> <li><strong>Dark Mode</strong>: I was going to say this is a silly thing to mention but the white light from three bright monitors in front of me is a little hard on my eyes--I much prefer dark edit screens. While the code editor in Delphi has had a variety of color schemes available since the beginning (greatly enhanced by the open source project, <a href="https://github.com/RRUZ/delphi-ide-theme-editor" rel="nofollow">Delphi IDE Theme Editor</a>), I was pleased when the whole of the Delphi IDE, including menus, dialogs, edit boxes, the object inspector, and other windowed areas were enhanced with an overall consistent theme applied. This took place late in 2017 with Delphi 10.2 Tokyo's second release.</li> <li><strong>Native Compiler</strong>: Ever since the early days of Turbo Pascal, which created small .COM files, this line of Pascal compilers has created tight, native compiled code. Even after going cross-platform, the generated applications have been relatively free from runtime environments which are necessary for Java or web apps. Even .NET has the CLR and it's arguably better than the JRE (Java Runtime Environment) and can now be compiled natively (from what I understand). But us Delphi developers seldom run into "DLL Hell" problems or experience application crashes because some underlying framework ot updated (unless you had to use anything related to Internet Explorer); we've been relatively free from these types of pitfalls plaguing other tools.</li> <li><strong>Platform support</strong>: With XE2 in 2011 came 64-bit Windows support and soon after that MacOS, iOS, and Android. In the early days, we never would've guessed we would be able take our skills and move them to support other platforms. Sure we've had web server support for a long time but simply spitting out HTML from a console-based Windows app is a far cry from producing mobile apps that look and feel native from a single code-base.</li> <li><strong>Project Manager</strong>: I like being able to load multiple projects, save them together in a project group, and change options such as configuration or platform for all projects at once with the Project Manager toolbar buttons. This feature has come in handy when dealing with a program with several plug-ins that are managed as a single application suite.</li> <li><strong>Webinars, books, and forums</strong>: While Delphi isn't the most popular development tool on the planet, it definitely has enough of a following to provide plenty of resources for learning the language and discussing issues with fellow programmers. The <a href="https://en.delphipraxis.net" rel="nofollow">Delphi-PRAXiS</a> forum is the most popular place to ask questions--and get plenty of answers. And there's a website devoted just to the <a href="https://delphi-books.com" rel="nofollow">books available for Delphi</a>.</li> <li><strong>Free code!</strong> The community includes developers that give back as well. There are a plethora of GitHub repositories and collections of repositories including <a href="https://github.com/jimmckeeth" rel="nofollow">Jim McKeeth</a>, <a href="https://github.com/DelphiWorlds" rel="nofollow">DelphiWorlds</a>, <a href="https://github.com/danieleteti" rel="nofollow">Daniele Teti</a>, <a href="https://github.com/andrea-magni" rel="nofollow">Andrea Magni</a>, <a href="https://github.com/HeidiSQL" rel="nofollow">HeidiSQL</a>, <a href="https://github.com/skia4delphi" rel="nofollow">Skia4Delphi</a>, <a href="https://github.com/Fr0sT-Brutal/awesome-pascal" rel="nofollow">Awesome Pascal</a>, <a href="https://github.com/graphics32" rel="nofollow">Graphics32</a>, <a href="https://github.com/DelphiPackageManager" rel="nofollow">Delphi Package Manager</a>, <a href="https://github.com/Embarcadero" rel="nofollow">Embarcadero Technologies</a>, <a href="https://github.com/IndySockets" rel="nofollow">Internet Direct</a>, <a href="https://github.com/pleriche/FastMM4" rel="nofollow">FastMM4</a>, <a href="https://github.com/VSoftTechnologies/DUnitX" rel="nofollow">DUnitX</a>, and many more (including <a href="https://github.com/corneliusdavid" rel="nofollow">my own</a>).</li> </ul> <p>What about Delphi are you thankful for?</p></div> <section id="node-comment"> <article data-comment-user-id="0" id="comment-526" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1668696854"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/526#comment-526" class="permalink" rel="bookmark" hreflang="en">Backwards compatibility</a></h3> <div class="single-comment-meta"> <span><a rel="nofollow" href="https://blog.dummzeuch.de" lang="" typeof="schema:Person" property="schema:name" datatype="">Thomas Müller (not verified)</a> Wed, 11/16/2022 - 23:59</span> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>Compatibility of programs compiled with Delphi 5 to the latest Windows versions is not an accomplishment of Delphi but of Windows. Microsoft has put a lot of effort into making Windows backwards compatible to old 32 bit software even to the point of breaking^D^D^D^D^D^D^D^Dadapting the security concepts for some of them so they can even continue to write to the program files and Windows directory, which has not been allowed for normal programs since Windows 2000/XP 20 years ago.</p> <p>Having said that: Yes, the backwards compatibility of the Delphi IDE to older versions is always astonishing.</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=526&amp;1=default&amp;2=en&amp;3=" token="Irm7tWTm38OiIyD373J9Wtfg7S28oUHaRR7JH6qrZgk"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="indented"> <article data-comment-user-id="1" id="comment-527" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/1"> </article> <mark class="hidden" data-comment-timestamp="1668697310"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/527#comment-527" class="permalink" rel="bookmark" hreflang="en">True</a></h3> <div class="single-comment-meta"> <span><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span> Thu, 11/17/2022 - 07:01</span> <p class="visually-hidden">In reply to <a href="/comment/526#comment-526" class="permalink" rel="bookmark" hreflang="en">Backwards compatibility</a> by <a rel="nofollow" href="https://blog.dummzeuch.de" lang="" typeof="schema:Person" property="schema:name" datatype="">Thomas Müller (not verified)</a></p> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>Yes, you're right--I almost mentioned that. I do recall, however, working on an old Visual BASIC app many years ago that actually broke because it relied on some Windows DLLs that were upgraded. I suppose that could happen with Delphi apps as well but I recall VB was especially vulnerable and many programmers lamented its lack of backwards compatibility but that may have been more in code upgrades than old apps working on newer versions--it gets kinda murky after a decade or two.</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=527&amp;1=default&amp;2=en&amp;3=" token="ZyG_cwySmsr88T7Qzk8BNjNBxSAUlvT0mEs2E5Iv0co"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> <div class="indented"> <article data-comment-user-id="0" id="comment-528" class="js-comment single-comment"> <header class="comment-user-picture"> <article typeof="schema:Person" about="/user/0"> </article> <mark class="hidden" data-comment-timestamp="1668919071"></mark> </header> <div class="single-comment-content-body"> <h3 class="single-comment-title"><a href="/comment/528#comment-528" class="permalink" rel="bookmark" hreflang="en">Visual Basic relied a lot…</a></h3> <div class="single-comment-meta"> <span><a rel="nofollow" href="https://blog.dummzeuch.de" lang="" typeof="schema:Person" property="schema:name" datatype="">Thomas Müller (not verified)</a> Sat, 11/19/2022 - 02:14</span> <p class="visually-hidden">In reply to <a href="/comment/527#comment-527" class="permalink" rel="bookmark" hreflang="en">True</a> by <span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></p> </div> <!-- /.single-comment-meta --> <div class="single-comment-content"> <div class="field field--name-comment-body field--type-text-long field--label-hidden field-item"><p>Visual Basic relied a lot more an binary extensions for components and functionality. While with Delphi, if you take/took care to always buy the source code with components you buy, you will always be able to move on to new IDE versions and - as long as Windows supports 32 bit applications - to new Windows version, in VB6 and earlier that was not possible. Not sure about nowadays, I never used VB.net.</p></div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=528&amp;1=default&amp;2=en&amp;3=" token="soVMg_uh7cTDAMOVyyUrKqJMpymNf9DkTQ7vrjh4p98"></drupal-render-placeholder> </div> </div> <!-- /.single-comment-content --> </article> </div></div> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=168&amp;2=comment_node_blog&amp;3=comment_node_blog" token="7m6GdlnVmrum-DYpLSUxZflKwYFRAJKCX7IBB9NegyI"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Wed, 16 Nov 2022 16:15:01 +0000 david 168 at https://corneliusconcepts.tech https://corneliusconcepts.tech/thankful-delphi#comments Resisting Windows 11 https://corneliusconcepts.tech/resisting-windows-11 <span property="dc:title">Resisting Windows 11</span> <div class="node-taxonomy-container"> <h3 class="term-title"><i class="icon-hashtag theme-color"></i> Category</h3> <ul class="taxonomy-terms"> <li class="taxonomy-term"><a href="/windows" hreflang="en">Windows</a></li> </ul> </div> <!--/.node-taxonomy-container --> <span rel="sioc:has_creator"><span lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="">david</span></span> <span property="dc:date dc:created" content="2022-10-16T20:26:12+00:00" datatype="xsd:dateTime">Sun, 10/16/2022 - 13:26</span> <div property="content:encoded" class="field field--name-body field--type-text-with-summary field--label-hidden field-item"><p>I like to keep all my computers upgraded with the latest versions of their respective operating systems. This goes for Mac and Android devices as well as all machines running Windows--even my virtual machines get the most recent upgrades whenever I use them. My development systems all use Windows but only one has been upgraded to Windows 11--the rest are still on Windows 10.</p> <p><img alt="Windows 10 Live Tiles" data-entity-type="file" data-entity-uuid="134b7fdd-4bb4-4e49-8968-f4b0daf2dcbf" src="/sites/default/files/inline-images/Win10Menu.png" width="450" class="align-right" />The reason isn't technical--the computers are compatible and I've used my Windows 11 laptop enough to feel confident it's plenty robust and ready for serious work--after all, Windows 11 has been out for over a year now. The main reason is actually kinda silly--but not uncommon: familiarity. People, by and large, are resistant to change and with change often comes a temporary reduction in productivity accompanied by a learning curve for doing things differently. I usually embrace new technology eagerly but the removal of Live Tiles in Windows 11 threw me a curve ball!</p> <p>The screenshot at the right shows some of the tiles I've placed and carefully arranged on my main development machine: Development IDEs and source control/analysis programs at the top, a section for databases underneath, and a long assortment of utilities and tools in the next column. I like to keep my desktop clean so don't want these icons there and while there are many I just type and select from the search list, there are some seldom-used tools that I want displayed in this list to remind me that I have them--and what they're called. So the Live Tiles list has become quite useful to me--not because I need information updated on the tile themselves but just so I have a list of icons to quickly see and access all the applications installed.</p> <p><strong>Windows 11 took that away.</strong></p> <p>We're now relegated to sets of 18 pinned apps with simple icons that can't be resized or grouped. You can't see all the icons at once and you can't even scroll slowly through them--you simply have a set of icons you can page through, 18 at a time. Of course, you can click the <em>All Apps</em> button and get a big list of everything that's been installed but that's not quick and concise like the curated list of tiles I had before.</p> <p>Yes, there are alternate solutions like <a href="https://apps.microsoft.com/store/detail/live-tiles-anywhere/9NR7QQK712PL?hl=en-us&amp;gl=us" rel="nofollow">Live Tiles Anywhere</a> that provide live tiles functionality on your desktop or pinned to your start menu or  task bar. There are also Start Menu replacements like <a href="https://www.stardock.com/products/start11/" rel="nofollow">Start11</a> or <a href="https://www.startallback.com/" rel="nofollow">StartAllBack</a>. I'll probably end up trying one of these to give me the list of tiled menu items I've come to like and depend on.</p> <p>For now, I'm dealing with three pages of icons on my laptop:</p> <p><img alt="Windows 11 Start Menu" data-entity-type="file" data-entity-uuid="10fe3538-2663-41f1-849d-7a6246af06c5" src="/sites/default/files/inline-images/Win11Menu.png" width="650" /></p> <p><strong>EDIT (2022-Oct-17):</strong></p> <p>I just discovered there's a Start menu Personalization option to show "More pins" in Windows 11. This adds one more row of pinned apps making the total available 24 instead of 18 as shown in this screenshot:</p> <p><img alt="Start menu with 24 pinned apps" data-entity-type="file" data-entity-uuid="9299edc7-7c75-4028-9faa-64e997f06e3a" src="/sites/default/files/inline-images/StartMorePinned.png" width="900" /></p></div> <section id="node-comment"> <div class="comment-form-wrap"> <h2 class="add-comment-title"><i class="icon-add_comment theme-color"></i> Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=167&amp;2=comment_node_blog&amp;3=comment_node_blog" token="Dc0iXMYoLJCLhV_4ei9LOPr5u0hXwMOtqzywAySeswk"></drupal-render-placeholder> </div> <!--/.comment-form --> </section> Sun, 16 Oct 2022 20:26:12 +0000 david 167 at https://corneliusconcepts.tech https://corneliusconcepts.tech/resisting-windows-11#comments