On recent weeks I’ve had some time to scratch my own itch on matters related to tools I use daily on my computer, namely the desktop / window manager and my text editor of choice.

This post is a summary of what I tried, how it worked out and my short and medium-term plans related to them.

Desktop / WM

On the desktop / window manager front I’ve been using Cinnamon on Debian and Ubuntu systems since Gnome 3 was published (I never liked version 3, so I decided to move to something similar to Gnome 2, including the keyboard shortcuts).

In fact I’ve never been a fan of Desktop environments, before Gnome I used OpenBox and IceWM because they where a lot faster than desktop systems on my hardware at the time and I was using them only to place one or two windows on multiple workspaces using mainly the keyboard for my interactions (well, except for the web browsers and the image manipulation programs).

Although I was comfortable using Cinnamon, some years ago I tried to move to i3, a tilling window manager for X11 that looked like a good choice for me, but I didn’t have much time to play with it and never used it enough to make me productive with it (I didn’t prepare a complete configuration nor had enough time to learn the new shortcuts, so I went back to Cinnamon and never tried again).

Anyway, some weeks ago I updated my work machine OS (it was using Ubuntu 22.04 LTS and I updated it to the 24.04 LTS version) and the Cinnamon systray applet stopped working as it used to do (in fact I still have to restart Cinnamon after starting a session to make it work) and, as I had some time, I decided to try a tilling window manager again, but now I decided to go for SwayWM, as it uses Wayland instead of X11.

Sway configuration

On my ~/.config/sway/config I tuned some things:

  • Set fuzzel as the application launcher.
  • Installed manually the shikane application and created a configuration to be executed always when sway is started / reloaded (I adjusted my configuration with wdisplays and used shikanectl to save it).
  • Added support for running the xdg-desktop-portal-wlr service.
  • Enabled the swayidle command to lock the screen after some time of inactivity.
  • Adjusted the keyboard to use the es key map
  • Added some keybindings to make my life easier, including the use of grimm and swappy to take screenshots
  • Configured waybar as the environment bar.
  • Added a shell script to start applications when sway is started (it uses swaymsg to execute background commands and the i3toolwait script to wait for the

    #!/bin/sh
    
    # VARIABLES
    CHROMIUM_LOCAL_STATE="$HOME/.config/google-chrome/Local State"
    I3_TOOLWAIT="$HOME/.config/sway/scripts/i3-toolwait"
    
    # Functions
    chromium_profile_dir() {
      jq -r ".profile.info_cache|to_entries|map({(.value.name): .key})|add|.\"$1\" // \"\"" "$CHROMIUM_LOCAL_STATE"
    }
    
    # MAIN
    IGZ_PROFILE_DIR="$(chromium_profile_dir "sergio.talens@intelygenz.com")"
    OURO_PROFILE_DIR="$(chromium_profile_dir "sergio.talens@nxr.global")"
    PERSONAL_PROFILE_DIR="$(chromium_profile_dir "stalens@gmail.com")"
    
    # Common programs
    swaymsg "exec nextcloud --background"
    swaymsg "exec nm-applet"
    
    # Run spotify on the first workspace (it is mapped to the laptop screen)
    swaymsg -q "workspace 1"
    ${I3_TOOLWAIT} "spotify"
    
    # Run tmux on the
    swaymsg -q "workspace 2"
    ${I3_TOOLWAIT} -- foot tmux a -dt sto
    
    wp_num="3"
    if [ "$OURO_PROFILE_DIR" ]; then
      swaymsg -q "workspace $wp_num"
      ${I3_TOOLWAIT} -m ouro-browser -- google-chrome --profile-directory="$OURO_PROFILE_DIR"
      wp_num="$((wp_num+1))"
    fi
    
    if [ "$IGZ_PROFILE_DIR" ]; then
      swaymsg -q "workspace $wp_num"
      ${I3_TOOLWAIT} -m igz-browser -- google-chrome --profile-directory="$IGZ_PROFILE_DIR"
      wp_num="$((wp_num+1))"
    fi
    
    if [ "$PERSONAL_PROFILE_DIR" ]; then
      swaymsg -q "workspace $wp_num"
      ${I3_TOOLWAIT} -m personal-browser -- google-chrome --profile-directory="$PERSONAL_PROFILE_DIR"
      wp_num="$((wp_num+1))"
    fi
    
    # Open the browser without setting the profile directory if none was found
    if [ "$wp_num" = "3" ]; then
      swaymsg -q "workspace $wp_num"
      ${I3_TOOLWAIT} google-chrome
      wp_num="$((wp_num+1))"
    fi
    swaymsg -q "workspace $wp_num"
    ${I3_TOOLWAIT} evolution
    wp_num="$((wp_num+1))"
    
    swaymsg -q "workspace $wp_num"
    ${I3_TOOLWAIT} slack
    wp_num="$((wp_num+1))"
    
    # Open a private browser and a console in the last workspace
    swaymsg -q "workspace $wp_num"
    ${I3_TOOLWAIT} -- google-chrome --incognito
    ${I3_TOOLWAIT} foot
    
    # Go back to the second workspace for keepassxc
    swaymsg "workspace 2"
    ${I3_TOOLWAIT} keepassxc

Conclusion

After using Sway for some days I can confirm that it is a good choice for me, but some of the components needed to make it work as I want are too new and not available on the Ubuntu 24.04 LTS repositories, so I decided to go back to Cinnamon and try Sway again in the future, although I added more workspaces to my setup (now they are only available on the main monitor, the laptop screen is fixed while there is a big monitor connected), added some additional keyboard shortcuts and installed or updated some applets.

Text editor

When I started using Linux many years ago I used vi/vim and emacs as my text editors (vi for plain text and emacs for programming and editing HTML/XML), but eventually I moved to vim as my main text editor and I’ve been using it since (well, I moved to neovim some time ago, although I kept my old vim configuration).

To be fair I’m not as expert as I could be with vim, but I’m productive with it and it has many plugins that make my life easier on my machines, while keeping my ability to edit text and configurations on any system that has a vi compatible editor installed.

For work reasons I tried to use Visual Studio Code last year, but I’ve never really liked it and almost everything I do with it I can do with neovim (i. e. I even use copilot with it). Besides, I’m a heavy terminal user (I use tmux locally and via ssh) and I like to be able to use my text editor on my shell sessions, and code does not work like that.

The only annoying thing about vim/neovim is its configuration (well, the problem is that I have a very old one and probably should spend some time fixing and updating it), but, as I said, it’s been working well for me for a long time, so I never really had the motivation to do it.

Anyway, after finishing my desktop tests I saw that I had the Helix editor installed for some time but I never tried it, so I decided to give it a try and see if it could be a good replacement for neovim on my environments (the only drawback is that as it is not vi compatible, I would need to switch back to vi mode when working on remote systems, but I guess I could live with that).

I ran the helix tutorial and I liked it, so I decided to configure and install the Language Servers I can probably take advantage of on my daily work on my personal and work machines and see how it works.

Language server installations

A lot of manual installations are needed to get the language servers working what I did on my machines is more or less the following:

# AWK
sudo npm i -g 'awk-language-server@>=0.5.2'
# BASH
sudo apt-get install shellcheck shfmt
sudo npm i -g bash-language-server
# C/C++
sudo apt-get install clangd
# CSS, HTML, ESLint, JSON, SCS
sudo npm i -g vscode-langservers-extracted
# Docker
sudo npm install -g dockerfile-language-server-nodejs
# Docker compose
sudo npm install -g @microsoft/compose-language-service
# Helm
app="helm_ls_linux_amd64"
url="$(
  curl -s https://api.github.com/repos/mrjosh/helm-ls/releases/latest |
    jq -r ".assets[] | select(.name == \"$app\") | .browser_download_url"
)"
curl -L "$url" --output /tmp/helm_ls
sudo install /tmp/helm_ls /usr/local/bin
rm /tmp/helm_ls
# Markdown
app="marksman-linux-x64"
url="$(
  curl -s https://api.github.com/repos/artempyanykh/marksman/releases/latest |
    jq -r ".assets[] | select(.name == \"$app\") | .browser_download_url"
)"
curl -L "$url" --output /tmp/marksman
sudo install /tmp/marksman /usr/local/bin
rm /tmp/marksman
# Python
sudo npm i -g pyright
# Rust
rustup component add rust-analyzer
# SQL
sudo npm i -g sql-language-server
# Terraform
sudo apt-get install terraform-ls
# TOML
cargo install taplo-cli --locked --features lsp
# YAML
sudo npm install --global yaml-language-server
# JavaScript, TypeScript
sudo npm install -g typescript-language-server typescript
sudo npm install -g --save-dev --save-exact @biomejs/biome

Helix configuration

The helix configuration is done on a couple of toml files that are placed on the ~/.config/helix directory, the config.toml file I used is this one:

theme = "solarized_light"

[editor]
line-number = "relative"
mouse = false

[editor.statusline]
left = ["mode", "spinner"]
center = ["file-name"]
right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
separator = "│"
mode.normal = "NORMAL"
mode.insert = "INSERT"
mode.select = "SELECT"

[editor.cursor-shape]
insert = "bar"
normal = "block"
select = "underline"

[editor.file-picker]
hidden = false

[editor.whitespace]
render = "all"

[editor.indent-guides]
render = true
character = "╎" # Some characters that work well: "▏", "┆", "┊", "⸽"
skip-levels = 1

And to configure the language servers I used the following language-servers.toml file:

[[language]]
name = "go"
auto-format = true
formatter = { command = "goimports" }

[[language]]
name = "javascript"
language-servers = [
  "typescript-language-server", # optional
  "vscode-eslint-language-server",
]

[language-server.rust-analyzer.config.check]
command = "clippy"

[language-server.sql-language-server]
command = "sql-language-server"
args = ["up", "--method", "stdio"]

[[language]]
name = "sql"
language-servers = [ "sql-language-server" ]

[[language]]
name = "hcl"
language-servers = [ "terraform-ls" ]
language-id = "terraform"

[[language]]
name = "tfvars"
language-servers = [ "terraform-ls" ]
language-id = "terraform-vars"

[language-server.terraform-ls]
command = "terraform-ls"
args = ["serve"]

[[language]]
name = "toml"
formatter = { command = "taplo", args = ["fmt", "-"] }

[[language]]
name = "typescript"
language-servers = [
  "typescript-language-server",
  "vscode-eslint-language-server",
]

Neovim configuration

After a little while I noticed that I was going to need some time to get used to helix and the most interesting thing for me was the easy configuration and the language server integrations, but as I am already comfortable with neovim and just had installed the language server support tools on my machines I just need to configure them for neovim and I can keep using it for a while.

As I said my configuration is old, to configure neovim I have the following init.vim file on my ~/.config/nvim folder:

set runtimepath^=~/.vim runtimepath+=~/.vim/after
let &packpath=&runtimepath
source ~/.vim/vimrc
" load lua configuration
lua require('config')

With that configuration I keep my old vimrc (it is a little bit messy, but it works) and I use a lua configuration file for the language servers and some additional neovim plugins on the ~/.config/nvim/lua/config.lua file:

-- -----------------------
-- BEG: LSP Configurations
-- -----------------------
-- AWS (awk_ls)
require'lspconfig'.awk_ls.setup{}
-- Bash (bashls)
require'lspconfig'.bashls.setup{}
-- C/C++ (clangd)
require'lspconfig'.clangd.setup{}
-- CSS (cssls)
require'lspconfig'.cssls.setup{}
-- Docker (dockerls)
require'lspconfig'.dockerls.setup{}
-- Docker Compose
require'lspconfig'.docker_compose_language_service.setup{}
-- Golang (gopls)
require'lspconfig'.gopls.setup{}
-- Helm (helm_ls)
require'lspconfig'.helm_ls.setup{}
-- Markdown
require'lspconfig'.marksman.setup{}
-- Python (pyright)
require'lspconfig'.pyright.setup{}
-- Rust (rust-analyzer)
require'lspconfig'.rust_analyzer.setup{}
-- SQL (sqlls)
require'lspconfig'.sqlls.setup{}
-- Terraform (terraformls)
require'lspconfig'.terraformls.setup{}
-- TOML (taplo)
require'lspconfig'.taplo.setup{}
-- Typescript (ts_ls)
require'lspconfig'.ts_ls.setup{}
-- YAML (yamlls)
require'lspconfig'.yamlls.setup{
  settings = {
    yaml = {
      customTags = { "!reference sequence" }
    }
  }
}
-- -----------------------
-- END: LSP Configurations
-- -----------------------
-- ---------------------------------
-- BEG: Autocompletion configuration
-- ---------------------------------
-- Ref: https://github.com/neovim/nvim-lspconfig/wiki/Autocompletion
--
-- Pre requisites:
--
--   # Packer
--   git clone --depth 1 https://github.com/wbthomason/packer.nvim \
--      ~/.local/share/nvim/site/pack/packer/start/packer.nvim
--
--   # Start nvim and run :PackerSync or :PackerUpdate
-- ---------------------------------
local use = require('packer').use
require('packer').startup(function()
  use 'wbthomason/packer.nvim' -- Packer, useful to avoid removing it with PackerSync / PackerUpdate
  use 'neovim/nvim-lspconfig' -- Collection of configurations for built-in LSP client
  use 'hrsh7th/nvim-cmp' -- Autocompletion plugin
  use 'hrsh7th/cmp-nvim-lsp' -- LSP source for nvim-cmp
  use 'saadparwaiz1/cmp_luasnip' -- Snippets source for nvim-cmp
  use 'L3MON4D3/LuaSnip' -- Snippets plugin
end)
-- Add additional capabilities supported by nvim-cmp
local capabilities = require("cmp_nvim_lsp").default_capabilities()
local lspconfig = require('lspconfig')
-- Enable some language servers with the additional completion capabilities offered by nvim-cmp
local servers = { 'clangd', 'rust_analyzer', 'pyright', 'ts_ls' }
for _, lsp in ipairs(servers) do
  lspconfig[lsp].setup {
    -- on_attach = my_custom_on_attach,
    capabilities = capabilities,
  }
end
-- luasnip setup
local luasnip = require 'luasnip'
-- nvim-cmp setup
local cmp = require 'cmp'
cmp.setup {
  snippet = {
    expand = function(args)
      luasnip.lsp_expand(args.body)
    end,
  },
  mapping = cmp.mapping.preset.insert({
    ['<C-u>'] = cmp.mapping.scroll_docs(-4), -- Up
    ['<C-d>'] = cmp.mapping.scroll_docs(4), -- Down
    -- C-b (back) C-f (forward) for snippet placeholder navigation.
    ['<C-Space>'] = cmp.mapping.complete(),
    ['<CR>'] = cmp.mapping.confirm {
      behavior = cmp.ConfirmBehavior.Replace,
      select = true,
    },
    ['<Tab>'] = cmp.mapping(function(fallback)
      if cmp.visible() then
        cmp.select_next_item()
      elseif luasnip.expand_or_jumpable() then
        luasnip.expand_or_jump()
      else
        fallback()
      end
    end, { 'i', 's' }),
    ['<S-Tab>'] = cmp.mapping(function(fallback)
      if cmp.visible() then
        cmp.select_prev_item()
      elseif luasnip.jumpable(-1) then
        luasnip.jump(-1)
      else
        fallback()
      end
    end, { 'i', 's' }),
  }),
  sources = {
    { name = 'nvim_lsp' },
    { name = 'luasnip' },
  },
}
-- ---------------------------------
-- END: Autocompletion configuration
-- ---------------------------------

Conclusion

I guess I’ll keep helix installed and try it again on some of my personal projects to see if I can get used to it, but for now I’ll stay with neovim as my main text editor and learn the shortcuts to use it with the language servers.