[Feature idea] Map in the terminal #68

Open
opened 2026-10-05 03:18:58 +02:00 by adam · 19 comments
Owner

I've been experimenting a bit with showing a static node map in the terminal. It seems it's possible entirely with python and with the help of libsixel. I'm using iTerm on MacOS which supports sixel protocol, and which is quite old btw. Not sure what others are using and if their terminals support sixel.

So I used static map generator - py-staticmaps to generate the image below by manually adding node names and their coordinates.

The idea is to collect a dynamic in-memory array of all node names and their coordinates and if the user presses, say ^m, the image will be generated on the fly and will be shown "full-screen". I guess it could be made to zoom in and out, as the generation is quite quick, but it will require further research.

The map generator uses various providers APIs to get the map tiles, so in case you want to use it offline, you need to figure out how to pre-download the tiles and generate the map. I haven't got that deep tbh and if it can be done without modifying py-staticmaps at all.

I experimented also with converting the image into ASCII, but the map is unrecognisable 😁 Any thoughts on the idea?

EDIT: This is a list of the sixel supported terminals. Not sure how up-to-date it is, but it's a starting point.

Image Image
I've been experimenting a bit with showing a **static** node map in the terminal. It seems it's possible entirely with python and with the help of [libsixel](https://github.com/saitoha/libsixel). I'm using iTerm on MacOS which supports sixel protocol, and which is quite old btw. Not sure what others are using and if their terminals support sixel. So I used static map generator - [py-staticmaps](https://github.com/flopp/py-staticmaps) to generate the image below by manually adding node names and their coordinates. The idea is to collect a dynamic in-memory array of all node names and their coordinates and if the user presses, say ^m, the image will be generated on the fly and will be shown "full-screen". I guess it could be made to zoom in and out, as the generation is quite quick, but it will require further research. The map generator uses various providers APIs to get the map tiles, so in case you want to use it offline, you need to figure out how to pre-download the tiles and generate the map. I haven't got that deep tbh and if it can be done without modifying `py-staticmaps` at all. I experimented also with converting the image into ASCII, but the map is unrecognisable 😁 Any thoughts on the idea? EDIT: [This is a list of the sixel supported terminals](https://www.arewesixelyet.com). Not sure how up-to-date it is, but it's a starting point. <img width="837" height="498" alt="Image" src="https://github.com/user-attachments/assets/d7c6bd9a-8d12-45ac-8a3a-599c8cb4b830" /> <img width="912" height="525" alt="Image" src="https://github.com/user-attachments/assets/0e1c977a-934c-4c6f-ae98-3c19bb925cad" /> <!-- github-sync-id:3358150471 -->
Author
Owner

Ok, I have implemented the map in my fork, although it might have some bugs. You can show the map with Ctrl-u

You need to have libsixel installed on your system e.g. sudo apt install libsixel or brew install libsixel

The most obvious bug is that the map is visible until:

  1. a key is pressed OR
  2. a payload update arrives, which brings back the main panels

I'm no curses expert and I couldn't find a way to stop refreshing the screen until key is pressed.

NOTE: Since the pip package of py-staticmaps is old and uses old pillow version (9.0.0) with breaking changes, I've added the git repo, so that the latest code of py-staticmaps is pulled form there.

Ok, I have implemented the map [in my fork](https://github.com/varna9000/contact/), although it might have some bugs. You can show the map with `Ctrl-u` You need to have `libsixel` installed on your system e.g. `sudo apt install libsixel` or `brew install libsixel` The most obvious bug is that the map is visible until: 1. a key is pressed **OR** 2. a payload update arrives, which brings back the main panels I'm no curses expert and I couldn't find a way to stop refreshing the screen until key is pressed. NOTE: Since the pip package of `py-staticmaps` is old and uses old `pillow` version (9.0.0) with breaking changes, I've added the git repo, so that the latest code of `py-staticmaps` is pulled form there. <!-- github-comment-id:3236007922 -->
Author
Owner

This looks really cool, though one of the project goals is to keep it compatible with all systems. Currently the only required library is meshtastic.

This looks really cool, though one of the project goals is to keep it compatible with all systems. Currently the only required library is `meshtastic`. <!-- github-comment-id:3237086516 -->
Author
Owner

That is fine. I thought it's a cool idea to have some visuals in the terminal, as big part of the Meshtastic fun is to see where the others are.

That is fine. I thought it's a cool idea to have some visuals in the terminal, as big part of the Meshtastic fun is to see where the others are. <!-- github-comment-id:3237178008 -->
Author
Owner

I agree! I'm not opposed to visuals. It would be fun to have something like https://github.com/stephane-deraco/Termchart for metrics as mentioned in #184 . I just want to make sure we can run without complications on devices like an RPi and LuckFox. Maybe there's a lo-fi/retro mapping option?

I agree! I'm not opposed to visuals. It would be fun to have something like https://github.com/stephane-deraco/Termchart for metrics as mentioned in #184 . I just want to make sure we can run without complications on devices like an RPi and LuckFox. Maybe there's a lo-fi/retro mapping option? <!-- github-comment-id:3237263834 -->
Author
Owner

The only option I found, which looks really good is MapSCII, but it's javascript and requires nodejs. Sadly, I couldn't find anything that good in Python.

The only option I found, which looks really good is [MapSCII](https://github.com/rastapasta/mapscii), but it's javascript and requires nodejs. Sadly, I couldn't find anything that good in Python. <!-- github-comment-id:3237286436 -->
Author
Owner

I'd also be totally fine with a modules/plugins folder with options that are disabled by default. We could move scripts from a disabled folder to an enabled folder which is scanned at startup like how this one works: https://github.com/Murturtle/MeshLinkBeta

I'd also be totally fine with a modules/plugins folder with options that are disabled by default. We could move scripts from a `disabled` folder to an `enabled` folder which is scanned at startup like how this one works: https://github.com/Murturtle/MeshLinkBeta <!-- github-comment-id:3237315145 -->
Author
Owner

Doesn't necessarily have to be a folder either. Could be an enabled-modules.ini or something. Totally open to other ideas.

Doesn't necessarily have to be a folder either. Could be an enabled-modules.ini or something. Totally open to other ideas. <!-- github-comment-id:3237356411 -->
Author
Owner

I need to wrap my head around curses loop a bit more, because it would definitely influence the design of the plugins. For example now we have the hotkeys in ui/contact_ui.py, but for a new plugin hotkey, we need to "inject" it into that loop too.

I need to wrap my head around curses loop a bit more, because it would definitely influence the design of the plugins. For example now we have the hotkeys in ui/[contact_ui.py](https://github.com/pdxlocations/contact/blob/8d41a1e06004a2988a7c4b0f14104b6c913e60ab/contact/ui/contact_ui.py#L167), but for a new plugin hotkey, we need to "inject" it into that loop too. <!-- github-comment-id:3237385013 -->
Author
Owner

Thinking out loud: instead of a file or folder, we could have list or dictionary that contains enabled modules, the we could check for if "map" in modules: or something before importing additional libraries and when checking for ctrl-m. A dictionary could be nice because it could also contain configs for each module.

Thinking out loud: instead of a file or folder, we could have list or dictionary that contains enabled modules, the we could check for `if "map" in modules:` or something before importing additional libraries and when checking for ctrl-m. A dictionary could be nice because it could also contain configs for each module. <!-- github-comment-id:3237653985 -->
Author
Owner

Yes, that would be the right pythonic way. I've seen other project doing it in similar way. They have config.py with all the initial global variables/settings, which is then included into main.py. I think there will be some shuffling of modules to get this plugin system working.

If the plugin requires visualization of data, would it create a new window, or just hide/show as the payload log pane? I think the current setup is ergonomic - 3+1 panes. But an option for a full screen plugin would be nice as well e.g. for the map or for plotting some stats with ascii.

Yes, that would be the right pythonic way. I've seen other project doing it in similar way. They have `config.py` with all the initial global variables/settings, which is then included into `main.py`. I think there will be some shuffling of modules to get this plugin system working. If the plugin requires visualization of data, would it create a new window, or just hide/show as the payload log pane? I think the current setup is ergonomic - 3+1 panes. But an option for a full screen plugin would be nice as well e.g. for the map or for plotting some stats with ascii. <!-- github-comment-id:3237774951 -->
Author
Owner

FYI there is a new single_pane_mode available in the app settings, in case you haven't seen that. It currently requires a relaunch.

For modules I suppose it would depend on the visuals but full screen sounds like the way. OR we could navigate to the map using the arrows: Channels -> Chat -> Nodes -> Map (full screen) -> Channels

FYI there is a new `single_pane_mode` available in the app settings, in case you haven't seen that. It currently requires a relaunch. For modules I suppose it would depend on the visuals but full screen sounds like the way. OR we could navigate to the map using the arrows: Channels -> Chat -> Nodes -> Map (full screen) -> Channels <!-- github-comment-id:3237782573 -->
Author
Owner

I really don't know what's happening, but I'm happy to help

I really don't know what's happening, but I'm happy to help <!-- github-comment-id:3237784792 -->
Author
Owner

FYI there is a new single_pane_mode available in the app settings, in case you haven't seen that. It currently requires a relaunch.

hm, I haven't seen it but will have a look.

For modules I suppose it would depend on the visuals but full screen sounds like the way. OR we could navigate to the map using the arrows: Channels -> Chat -> Nodes -> Map (full screen) -> Channels

Cycling seems reasonable, but if you have more than one plugin enabled, would that be just queued e.g. Nodes -> Fullscreen map ->Fullscreen graphs -> Channels ?

> FYI there is a new `single_pane_mode` available in the app settings, in case you haven't seen that. It currently requires a relaunch. hm, I haven't seen it but will have a look. > For modules I suppose it would depend on the visuals but full screen sounds like the way. OR we could navigate to the map using the arrows: Channels -> Chat -> Nodes -> Map (full screen) -> Channels Cycling seems reasonable, but if you have more than one plugin enabled, would that be just queued e.g. Nodes -> Fullscreen map ->Fullscreen graphs -> Channels ? <!-- github-comment-id:3237812380 -->
Author
Owner

I've been asked to contribute to GitHub projects in order to gain experience, I'm a 16-year-old self-taught full stack developer, but I really don't know where to start, anyway here is my resume https://drive.google.com/file/d/1ttaXqyAOgU2IPNxLoiSZGZEqRr9m7xkt/view

I've been asked to contribute to GitHub projects in order to gain experience, I'm a 16-year-old self-taught full stack developer, but I really don't know where to start, anyway here is my resume https://drive.google.com/file/d/1ttaXqyAOgU2IPNxLoiSZGZEqRr9m7xkt/view <!-- github-comment-id:3237862895 -->
Author
Owner

Cycling seems reasonable, but if you have more than one plugin enabled, would that be just queued e.g. Nodes -> Fullscreen map ->Fullscreen graphs -> Channels ?

Would need some thinking and experimentation. Each module may have it's own design requirements. I haven't looked at your fork to be honest. Starting simple by checking for modules and using ctrl-m to bring up a full-screen map window might be the first course of action. I'm using ui_state.current_window = 4: to announce we have the settings dialogue displayed. Adding more active windows may require some refactor there.

> Cycling seems reasonable, but if you have more than one plugin enabled, would that be just queued e.g. Nodes -> Fullscreen map ->Fullscreen graphs -> Channels ? Would need some thinking and experimentation. Each module may have it's own design requirements. I haven't looked at your fork to be honest. Starting simple by checking for modules and using ctrl-m to bring up a full-screen map window might be the first course of action. I'm using `ui_state.current_window = 4:` to announce we have the settings dialogue displayed. Adding more active windows may require some refactor there. <!-- github-comment-id:3238058939 -->
Author
Owner

I've been asked to contribute to GitHub projects in order to gain experience, I'm a 16-year-old self-taught full stack developer, but I really don't know where to start, anyway here is my resume https://drive.google.com/file/d/1ttaXqyAOgU2IPNxLoiSZGZEqRr9m7xkt/view

Awesome! I'm not going to ask for help in any particular area, but if you see something that you'd like to work on, go for it!
I've just released a very incomplete and buggy UDP chat app for Meshtastic that might interest you as well: https://github.com/pdxlocations/mudpchat

I'm not a developer by trade and just do this for fun using lots of AI help. I'd suggest checking out the meshtastic.org repos if you want to contribute to a larger project. I help out with their docs and sometimes break their python library, lol.

> I've been asked to contribute to GitHub projects in order to gain experience, I'm a 16-year-old self-taught full stack developer, but I really don't know where to start, anyway here is my resume https://drive.google.com/file/d/1ttaXqyAOgU2IPNxLoiSZGZEqRr9m7xkt/view Awesome! I'm not going to ask for help in any particular area, but if you see something that you'd like to work on, go for it! I've just released a very incomplete and buggy UDP chat app for Meshtastic that might interest you as well: <https://github.com/pdxlocations/mudpchat> I'm not a developer by trade and just do this for fun using lots of AI help. I'd suggest checking out the meshtastic.org repos if you want to contribute to a larger project. I help out with their docs and sometimes break their python library, lol. <!-- github-comment-id:3238070483 -->
Author
Owner

Cycling seems reasonable, but if you have more than one plugin enabled, would that be just queued e.g. Nodes -> Fullscreen map ->Fullscreen graphs -> Channels ?

Would need some thinking and experimentation. Each module may have it's own design requirements. I haven't looked at your fork to be honest. Starting simple by checking for modules and using ctrl-m to bring up a full-screen map window might be the first course of action. I'm using ui_state.current_window = 4: to announce we have the settings dialogue displayed. Adding more active windows may require some refactor there.

I've been fighting with curses design and it seems the sixel bytes data is incompatible with curses. So the approach I'm using is temporary disable curses with stdscr.endwin(), print the sixel data to the original stdout to full screen and then go back to curses. So we need to have such "workaround" in mind as well. I'm experimenting now how to pass the key events from original stdout to curses loop, so we can have the original navigation working.

> > Cycling seems reasonable, but if you have more than one plugin enabled, would that be just queued e.g. Nodes -> Fullscreen map ->Fullscreen graphs -> Channels ? > > Would need some thinking and experimentation. Each module may have it's own design requirements. I haven't looked at your fork to be honest. Starting simple by checking for modules and using ctrl-m to bring up a full-screen map window might be the first course of action. I'm using `ui_state.current_window = 4:` to announce we have the settings dialogue displayed. Adding more active windows may require some refactor there. I've been fighting with curses design and it seems the sixel bytes data is incompatible with curses. So the approach I'm using is temporary disable curses with `stdscr.endwin()`, print the sixel data to the original stdout to full screen and then go back to curses. So we need to have such "workaround" in mind as well. I'm experimenting now how to pass the key events from original stdout to curses loop, so we can have the original navigation working. <!-- github-comment-id:3239020185 -->
Author
Owner

Ok, some updates on my map plugin. I had to refactor ui_contact.py and replace all auto screen .refresh() with the combo .noutrefresh() and .doupdate(). This gives more control when to update the physical screen and when not to do that.

Basically for the map plugin to work properly, I needed to stop screen update until user manually exits the fullscreen map mode. If I don't do that, any screen updates by other functions just force exit the map, not giving enough time to the user to look at it carefully and explore the node positions.

Now with .noutrefresh() and .doupdate() combo, if map_mode is not set, we are doing regular physical screen updates and everything works as normal. Once we set map_mode to true, physical screen updates are disabled, while we explore the map. When we hit any key, we unset map_mode and we go back to regular screen updates.

What I still need to do:

  1. Get the proper node name from the database and print that on the map, instead of the hex id
  2. Move this code into a plugin form, once we have the plugin structure

Please take a look at the refactored ui_contact.py when you do the plugin system refactoring, so we can keep .noutrefresh()/.doupdate() approach, instead of .refresh()

Ok, some updates on my map plugin. I had to refactor `ui_contact.py` and replace all auto screen `.refresh()` with the combo `.noutrefresh()` and `.doupdate()`. This gives more control when to update the physical screen and when not to do that. Basically for the map plugin to work properly, I needed to stop screen update until user manually exits the fullscreen map mode. If I don't do that, any screen updates by other functions just force exit the map, not giving enough time to the user to look at it carefully and explore the node positions. Now with `.noutrefresh()` and `.doupdate()` combo, if `map_mode` is not set, we are doing regular physical screen updates and everything works as normal. Once we set `map_mode` to true, physical screen updates are disabled, while we explore the map. When we hit any key, we unset `map_mode` and we go back to regular screen updates. What I still need to do: 1. Get the proper node name from the database and print that on the map, instead of the hex id 2. Move this code into a plugin form, once we have the plugin structure Please take a look at the refactored `ui_contact.py` when you do the plugin system refactoring, so we can keep `.noutrefresh()`/`.doupdate()` approach, instead of `.refresh()` <!-- github-comment-id:3239519763 -->
Author
Owner

I've got small update on the map plugin. Before rendering, we now read the short node names from the db and show them on the map. Also since the default PIL render font doesn't support emojis, I've added black and white emoji TrueType font (MIT licence), so it renders emoji in short node names on the map too.

Image
I've got small update on the map plugin. Before rendering, we now read the short node names from the db and show them on the map. Also since the default PIL render font doesn't support emojis, I've added black and white emoji TrueType font (MIT licence), so it renders emoji in short node names on the map too. <img width="157" height="155" alt="Image" src="https://github.com/user-attachments/assets/d6583ea1-3032-45de-9cf6-15915415cc4b" /> <!-- github-comment-id:3315070233 -->
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: adam/contact#68